@bubblebrain-ai/bubble 0.0.18 → 0.0.20
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/dist/agent/internal-reminder-sanitizer.d.ts +1 -0
- package/dist/agent/internal-reminder-sanitizer.js +46 -0
- package/dist/agent.d.ts +9 -0
- package/dist/agent.js +305 -17
- package/dist/approval/controller.d.ts +6 -0
- package/dist/approval/controller.js +104 -11
- package/dist/debug-trace.js +4 -0
- package/dist/feishu/agent-host/run-driver.js +28 -0
- package/dist/hooks/config.d.ts +9 -0
- package/dist/hooks/config.js +278 -0
- package/dist/hooks/controller.d.ts +24 -0
- package/dist/hooks/controller.js +254 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/log.d.ts +14 -0
- package/dist/hooks/log.js +54 -0
- package/dist/hooks/runner.d.ts +5 -0
- package/dist/hooks/runner.js +225 -0
- package/dist/hooks/trust.d.ts +37 -0
- package/dist/hooks/trust.js +143 -0
- package/dist/hooks/types.d.ts +173 -0
- package/dist/hooks/types.js +46 -0
- package/dist/main.js +32 -0
- package/dist/memory/prompts.js +3 -1
- package/dist/model-catalog.js +2 -0
- package/dist/model-pricing.js +8 -0
- package/dist/network/chatgpt-transport.js +34 -9
- package/dist/network/provider-transport.d.ts +32 -0
- package/dist/network/provider-transport.js +265 -0
- package/dist/network/retry.d.ts +29 -0
- package/dist/network/retry.js +88 -0
- package/dist/network/system-proxy.d.ts +18 -0
- package/dist/network/system-proxy.js +175 -0
- package/dist/provider-anthropic.d.ts +1 -0
- package/dist/provider-anthropic.js +127 -52
- package/dist/provider-openai-codex.js +19 -29
- package/dist/session-log.js +3 -3
- package/dist/slash-commands/commands.js +84 -0
- package/dist/slash-commands/types.d.ts +2 -0
- package/dist/tools/edit-apply.js +63 -3
- package/dist/tools/edit.js +4 -4
- package/dist/tui/display-history.d.ts +4 -3
- package/dist/tui/display-history.js +34 -57
- package/dist/tui/display-sanitizer.d.ts +3 -0
- package/dist/tui/display-sanitizer.js +38 -0
- package/dist/tui/paste-placeholder.d.ts +1 -0
- package/dist/tui/paste-placeholder.js +7 -0
- package/dist/tui/run.d.ts +2 -0
- package/dist/tui/run.js +260 -155
- package/dist/tui/trace-groups.js +40 -4
- package/dist/tui/wordmark.d.ts +1 -0
- package/dist/tui/wordmark.js +56 -54
- package/dist/tui-ink/app.js +2 -1
- package/dist/tui-ink/trace-groups.js +40 -4
- package/dist/tui-opentui/app.js +2 -1
- package/dist/tui-opentui/trace-groups.js +40 -4
- package/dist/types.d.ts +27 -0
- package/package.json +1 -1
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { delimiter } from "node:path";
|
|
3
|
+
import { rootCertificates } from "node:tls";
|
|
4
|
+
import { Agent, ProxyAgent } from "undici";
|
|
5
|
+
import { getSystemProxyForUrl } from "./system-proxy.js";
|
|
6
|
+
export function providerFetch(input, init, options) {
|
|
7
|
+
return createProviderFetch(options)(input, init);
|
|
8
|
+
}
|
|
9
|
+
export function createProviderFetch(options) {
|
|
10
|
+
const env = options.env ?? process.env;
|
|
11
|
+
const fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
12
|
+
return async (input, init) => {
|
|
13
|
+
try {
|
|
14
|
+
const requestInit = withProviderNetworkOptions(input, init, {
|
|
15
|
+
env,
|
|
16
|
+
providerName: options.providerName,
|
|
17
|
+
verboseEnvVar: options.verboseEnvVar,
|
|
18
|
+
});
|
|
19
|
+
return await fetchImpl(input, requestInit);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
throw normalizeProviderNetworkError(error, {
|
|
23
|
+
providerName: options.providerName,
|
|
24
|
+
input,
|
|
25
|
+
env,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function createProviderDispatcher(env = process.env, input, providerName = "provider") {
|
|
31
|
+
if (isBunRuntime())
|
|
32
|
+
return undefined;
|
|
33
|
+
const ca = loadExtraCaCertificates(env, providerName);
|
|
34
|
+
const proxy = input ? nodeProxyForUrl(input, env) : defaultNodeProxy(env);
|
|
35
|
+
if (!proxy && ca.length === 0)
|
|
36
|
+
return undefined;
|
|
37
|
+
const caOptions = ca.length > 0 ? { ca: [...rootCertificates, ...ca] } : undefined;
|
|
38
|
+
if (proxy) {
|
|
39
|
+
return new ProxyAgent({
|
|
40
|
+
uri: proxy,
|
|
41
|
+
...(caOptions ? { requestTls: caOptions, proxyTls: caOptions } : {}),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return caOptions ? new Agent({ connect: caOptions }) : undefined;
|
|
45
|
+
}
|
|
46
|
+
export function withProviderNetworkOptions(input, init, options = {}) {
|
|
47
|
+
const env = options.env ?? process.env;
|
|
48
|
+
const providerName = options.providerName ?? "provider";
|
|
49
|
+
const next = { ...(init ?? {}) };
|
|
50
|
+
if (isBunRuntime()) {
|
|
51
|
+
const proxy = bunProxyForUrl(input, env);
|
|
52
|
+
if (proxy)
|
|
53
|
+
next.proxy = proxy;
|
|
54
|
+
const ca = bunExtraCaFiles(env);
|
|
55
|
+
if (ca.length > 0)
|
|
56
|
+
next.tls = { ...(next.tls ?? {}), ca };
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const dispatcher = createProviderDispatcher(env, input, providerName);
|
|
60
|
+
if (dispatcher)
|
|
61
|
+
next.dispatcher = dispatcher;
|
|
62
|
+
}
|
|
63
|
+
if (shouldEnableFetchVerbose(env, options.verboseEnvVar)) {
|
|
64
|
+
next.verbose = true;
|
|
65
|
+
}
|
|
66
|
+
return next;
|
|
67
|
+
}
|
|
68
|
+
export function normalizeProviderNetworkError(error, options) {
|
|
69
|
+
const env = options.env ?? process.env;
|
|
70
|
+
// Already normalized (e.g. by createProviderFetch) — wrapping again would
|
|
71
|
+
// nest "Original error:" messages.
|
|
72
|
+
if (error instanceof Error && error.message.includes("connection failed before Bubble received a response")) {
|
|
73
|
+
return error;
|
|
74
|
+
}
|
|
75
|
+
const text = errorMessageChain(error).join("\n");
|
|
76
|
+
if (!isProviderNetworkErrorText(text)) {
|
|
77
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
78
|
+
}
|
|
79
|
+
const origin = originFromInput(options.input);
|
|
80
|
+
const providerLabel = options.providerName || "provider";
|
|
81
|
+
const systemProxy = hasProxyEnv(env) ? undefined : getSystemProxyForUrl(options.input ? urlFromInput(options.input) : undefined, env);
|
|
82
|
+
const message = [
|
|
83
|
+
`${providerLabel} connection failed before Bubble received a response.`,
|
|
84
|
+
origin ? `Target origin: ${origin}.` : undefined,
|
|
85
|
+
isCertificateErrorText(text)
|
|
86
|
+
? "TLS certificate verification failed. If you are on a corporate proxy, VPN, or HTTPS inspection network, start Bubble with NODE_EXTRA_CA_CERTS=/absolute/path/to/ca.pem or BUBBLE_EXTRA_CA_CERTS=/absolute/path/to/ca.pem."
|
|
87
|
+
: "This looks like a proxy or network transport failure.",
|
|
88
|
+
hasProxyEnv(env)
|
|
89
|
+
? "Bubble is using proxy environment variables for provider requests. Make sure NO_PROXY includes localhost,127.0.0.1 and any direct-connect hosts."
|
|
90
|
+
: systemProxy
|
|
91
|
+
? `Bubble is routing this request through the OS system proxy at ${systemProxy} (detected automatically). Check that the proxy app is running and healthy, or set BUBBLE_SYSTEM_PROXY=0 to disable system proxy detection.`
|
|
92
|
+
: "If your network requires a proxy, set HTTPS_PROXY or HTTP_PROXY, and set NO_PROXY=localhost,127.0.0.1.",
|
|
93
|
+
hasCustomCaEnv(env)
|
|
94
|
+
? "A custom CA environment variable is configured."
|
|
95
|
+
: "No custom CA environment variable is configured.",
|
|
96
|
+
"Do not disable TLS verification with NODE_TLS_REJECT_UNAUTHORIZED=0.",
|
|
97
|
+
`Original error: ${firstMeaningfulErrorMessage(error) || "unknown network error"}`,
|
|
98
|
+
].filter(Boolean).join(" ");
|
|
99
|
+
return new Error(message, { cause: error });
|
|
100
|
+
}
|
|
101
|
+
export function isProviderTransportError(error) {
|
|
102
|
+
return isProviderNetworkErrorText(errorMessageChain(error).join("\n"));
|
|
103
|
+
}
|
|
104
|
+
export function shouldEnableFetchVerbose(env = process.env, providerVerboseEnvVar) {
|
|
105
|
+
const providerValue = providerVerboseEnvVar ? env[providerVerboseEnvVar] : undefined;
|
|
106
|
+
return isTruthyEnv(providerValue) || isTruthyEnv(env.BUBBLE_PROVIDER_FETCH_VERBOSE);
|
|
107
|
+
}
|
|
108
|
+
function hasProxyEnv(env) {
|
|
109
|
+
return Boolean(env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy || env.ALL_PROXY || env.all_proxy);
|
|
110
|
+
}
|
|
111
|
+
function hasCustomCaEnv(env) {
|
|
112
|
+
return Boolean(env.NODE_EXTRA_CA_CERTS?.trim() || env.BUBBLE_EXTRA_CA_CERTS?.trim());
|
|
113
|
+
}
|
|
114
|
+
function isBunRuntime() {
|
|
115
|
+
return typeof globalThis.Bun !== "undefined";
|
|
116
|
+
}
|
|
117
|
+
function bunProxyForUrl(input, env) {
|
|
118
|
+
const url = urlFromInput(input);
|
|
119
|
+
if (!url || shouldBypassProxy(url, env))
|
|
120
|
+
return undefined;
|
|
121
|
+
const allProxy = env.ALL_PROXY ?? env.all_proxy;
|
|
122
|
+
if (url.protocol === "https:")
|
|
123
|
+
return env.HTTPS_PROXY ?? env.https_proxy ?? allProxy ?? getSystemProxyForUrl(url, env);
|
|
124
|
+
if (url.protocol === "http:")
|
|
125
|
+
return env.HTTP_PROXY ?? env.http_proxy ?? allProxy ?? getSystemProxyForUrl(url, env);
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
function nodeProxyForUrl(input, env) {
|
|
129
|
+
const url = urlFromInput(input);
|
|
130
|
+
if (!url || shouldBypassProxy(url, env))
|
|
131
|
+
return undefined;
|
|
132
|
+
if (url.protocol === "https:")
|
|
133
|
+
return env.HTTPS_PROXY ?? env.https_proxy ?? env.ALL_PROXY ?? env.all_proxy ?? getSystemProxyForUrl(url, env);
|
|
134
|
+
if (url.protocol === "http:")
|
|
135
|
+
return env.HTTP_PROXY ?? env.http_proxy ?? env.ALL_PROXY ?? env.all_proxy ?? getSystemProxyForUrl(url, env);
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
function defaultNodeProxy(env) {
|
|
139
|
+
return env.HTTPS_PROXY ?? env.https_proxy ?? env.HTTP_PROXY ?? env.http_proxy ?? env.ALL_PROXY ?? env.all_proxy
|
|
140
|
+
?? getSystemProxyForUrl(new URL("https://system-proxy-default.invalid/"), env);
|
|
141
|
+
}
|
|
142
|
+
function bunExtraCaFiles(env) {
|
|
143
|
+
const bun = globalThis.Bun;
|
|
144
|
+
if (!bun?.file)
|
|
145
|
+
return [];
|
|
146
|
+
return extraCaCertificatePaths(env).map((path) => bun.file(path));
|
|
147
|
+
}
|
|
148
|
+
function urlFromInput(input) {
|
|
149
|
+
if (input instanceof URL)
|
|
150
|
+
return input;
|
|
151
|
+
if (typeof input === "string")
|
|
152
|
+
return URL.canParse(input) ? new URL(input) : undefined;
|
|
153
|
+
const url = input.url;
|
|
154
|
+
return URL.canParse(url) ? new URL(url) : undefined;
|
|
155
|
+
}
|
|
156
|
+
function originFromInput(input) {
|
|
157
|
+
if (!input)
|
|
158
|
+
return undefined;
|
|
159
|
+
return urlFromInput(input)?.origin;
|
|
160
|
+
}
|
|
161
|
+
function shouldBypassProxy(url, env) {
|
|
162
|
+
const noProxy = (env.NO_PROXY ?? env.no_proxy ?? "").trim();
|
|
163
|
+
if (!noProxy)
|
|
164
|
+
return false;
|
|
165
|
+
if (noProxy === "*")
|
|
166
|
+
return true;
|
|
167
|
+
const hostname = url.hostname.toLowerCase();
|
|
168
|
+
const port = url.port;
|
|
169
|
+
return noProxy
|
|
170
|
+
.split(/[,\s]+/)
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.some((entry) => noProxyEntryMatches(entry.toLowerCase(), hostname, port));
|
|
173
|
+
}
|
|
174
|
+
function noProxyEntryMatches(entry, hostname, port) {
|
|
175
|
+
const [entryHost, entryPort] = entry.includes(":") ? entry.split(":") : [entry, ""];
|
|
176
|
+
if (entryPort && entryPort !== port)
|
|
177
|
+
return false;
|
|
178
|
+
if (entryHost === hostname)
|
|
179
|
+
return true;
|
|
180
|
+
if (entryHost.startsWith("*."))
|
|
181
|
+
return hostname.endsWith(entryHost.slice(1));
|
|
182
|
+
if (entryHost.startsWith("."))
|
|
183
|
+
return hostname.endsWith(entryHost);
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
function loadExtraCaCertificates(env, providerName) {
|
|
187
|
+
const paths = extraCaCertificatePaths(env);
|
|
188
|
+
return paths.map((path) => {
|
|
189
|
+
try {
|
|
190
|
+
return readFileSync(path, "utf-8");
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
throw new Error(`Failed to read ${providerName} custom CA certificate at ${path}. Check NODE_EXTRA_CA_CERTS or BUBBLE_EXTRA_CA_CERTS.`, {
|
|
194
|
+
cause: error,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function extraCaCertificatePaths(env) {
|
|
200
|
+
const bubbleValue = env.BUBBLE_EXTRA_CA_CERTS?.trim();
|
|
201
|
+
if (bubbleValue) {
|
|
202
|
+
return bubbleValue.split(delimiter).map((item) => item.trim()).filter(Boolean);
|
|
203
|
+
}
|
|
204
|
+
const nodeValue = env.NODE_EXTRA_CA_CERTS?.trim();
|
|
205
|
+
return nodeValue ? [nodeValue] : [];
|
|
206
|
+
}
|
|
207
|
+
function isProviderNetworkErrorText(text) {
|
|
208
|
+
return [
|
|
209
|
+
/fetch failed/i,
|
|
210
|
+
/network.*failed/i,
|
|
211
|
+
/socket connection was closed unexpectedly/i,
|
|
212
|
+
/\bConnectionClosed\b/i,
|
|
213
|
+
/\bECONNRESET\b/i,
|
|
214
|
+
/\bECONNREFUSED\b/i,
|
|
215
|
+
/\bETIMEDOUT\b/i,
|
|
216
|
+
/\bEPIPE\b/i,
|
|
217
|
+
/\bUND_ERR_/i,
|
|
218
|
+
/socket hang up/i,
|
|
219
|
+
/Unable to connect\. Is the computer able to access the url\?/i,
|
|
220
|
+
/certificate/i,
|
|
221
|
+
/unable to verify/i,
|
|
222
|
+
/self[- ]signed/i,
|
|
223
|
+
].some((pattern) => pattern.test(text));
|
|
224
|
+
}
|
|
225
|
+
function isCertificateErrorText(text) {
|
|
226
|
+
return [
|
|
227
|
+
/unknown certificate verification error/i,
|
|
228
|
+
/certificate (?:verify|verification) (?:failed|error)/i,
|
|
229
|
+
/unable to verify (?:the )?(?:first )?certificate/i,
|
|
230
|
+
/UNABLE_TO_(?:VERIFY_LEAF_SIGNATURE|GET_ISSUER_CERT_LOCALLY)/i,
|
|
231
|
+
/SELF_SIGNED_CERT_IN_CHAIN/i,
|
|
232
|
+
/DEPTH_ZERO_SELF_SIGNED_CERT/i,
|
|
233
|
+
/CERT_(?:HAS_EXPIRED|UNTRUSTED|INVALID)/i,
|
|
234
|
+
/self[- ]signed certificate/i,
|
|
235
|
+
].some((pattern) => pattern.test(text));
|
|
236
|
+
}
|
|
237
|
+
function firstMeaningfulErrorMessage(error) {
|
|
238
|
+
return errorMessageChain(error).find((item) => item && item !== "Error");
|
|
239
|
+
}
|
|
240
|
+
function errorMessageChain(error) {
|
|
241
|
+
const messages = [];
|
|
242
|
+
let current = error;
|
|
243
|
+
for (let depth = 0; current && depth < 8; depth++) {
|
|
244
|
+
if (current instanceof Error) {
|
|
245
|
+
messages.push(current.name, current.message);
|
|
246
|
+
current = current.cause;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (typeof current === "object") {
|
|
250
|
+
const record = current;
|
|
251
|
+
for (const key of ["name", "code", "message"]) {
|
|
252
|
+
if (typeof record[key] === "string")
|
|
253
|
+
messages.push(record[key]);
|
|
254
|
+
}
|
|
255
|
+
current = record.cause;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
messages.push(String(current));
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
return messages;
|
|
262
|
+
}
|
|
263
|
+
function isTruthyEnv(value) {
|
|
264
|
+
return /^(1|true|yes)$/i.test(value ?? "");
|
|
265
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared retry policy for provider transports.
|
|
3
|
+
*
|
|
4
|
+
* Connection-level failures (nothing received yet) and retryable HTTP
|
|
5
|
+
* statuses are retried inside the provider with exponential backoff.
|
|
6
|
+
* Mid-stream interruptions (content already surfaced to the UI) are
|
|
7
|
+
* signalled with ProviderStreamInterruptedError so the agent loop can
|
|
8
|
+
* discard the partial assistant message and re-issue the whole request.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MAX_STREAM_INTERRUPTION_RETRIES = 2;
|
|
11
|
+
export declare class ProviderStreamInterruptedError extends Error {
|
|
12
|
+
readonly isProviderStreamInterruption = true;
|
|
13
|
+
constructor(message: string, options?: {
|
|
14
|
+
cause?: unknown;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export declare function isProviderStreamInterruption(error: unknown): boolean;
|
|
18
|
+
export declare function getProviderMaxRetries(env?: NodeJS.ProcessEnv): number;
|
|
19
|
+
export declare function isRetryableHttpStatus(status: number): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Equal-jitter exponential backoff: attempt 1 → 0.5-1s, 2 → 1-2s, 3 → 2-4s,
|
|
22
|
+
* 4 → 4-8s, capped at 32s. A retry-after hint from the server wins (capped
|
|
23
|
+
* at 60s) since it reflects actual load shedding.
|
|
24
|
+
*/
|
|
25
|
+
export declare function computeRetryDelayMs(attempt: number, options?: {
|
|
26
|
+
retryAfterMs?: number;
|
|
27
|
+
}): number;
|
|
28
|
+
export declare function retryAfterMsFromResponse(response: Response): number | undefined;
|
|
29
|
+
export declare function sleepBeforeRetry(ms: number, signal?: AbortSignal): Promise<void>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared retry policy for provider transports.
|
|
3
|
+
*
|
|
4
|
+
* Connection-level failures (nothing received yet) and retryable HTTP
|
|
5
|
+
* statuses are retried inside the provider with exponential backoff.
|
|
6
|
+
* Mid-stream interruptions (content already surfaced to the UI) are
|
|
7
|
+
* signalled with ProviderStreamInterruptedError so the agent loop can
|
|
8
|
+
* discard the partial assistant message and re-issue the whole request.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_MAX_RETRIES = 4;
|
|
11
|
+
const MAX_CONFIGURABLE_RETRIES = 10;
|
|
12
|
+
const BASE_DELAY_MS = 1000;
|
|
13
|
+
const MAX_DELAY_MS = 32_000;
|
|
14
|
+
const MAX_RETRY_AFTER_MS = 60_000;
|
|
15
|
+
export const MAX_STREAM_INTERRUPTION_RETRIES = 2;
|
|
16
|
+
const RETRYABLE_HTTP_STATUSES = new Set([408, 429, 500, 502, 503, 504, 529]);
|
|
17
|
+
export class ProviderStreamInterruptedError extends Error {
|
|
18
|
+
isProviderStreamInterruption = true;
|
|
19
|
+
constructor(message, options) {
|
|
20
|
+
super(message, options);
|
|
21
|
+
this.name = "ProviderStreamInterruptedError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function isProviderStreamInterruption(error) {
|
|
25
|
+
return !!error
|
|
26
|
+
&& typeof error === "object"
|
|
27
|
+
&& error.isProviderStreamInterruption === true;
|
|
28
|
+
}
|
|
29
|
+
export function getProviderMaxRetries(env = process.env) {
|
|
30
|
+
const raw = env.BUBBLE_PROVIDER_MAX_RETRIES?.trim();
|
|
31
|
+
if (!raw)
|
|
32
|
+
return DEFAULT_MAX_RETRIES;
|
|
33
|
+
const value = Number(raw);
|
|
34
|
+
if (!Number.isInteger(value) || value < 0)
|
|
35
|
+
return DEFAULT_MAX_RETRIES;
|
|
36
|
+
return Math.min(value, MAX_CONFIGURABLE_RETRIES);
|
|
37
|
+
}
|
|
38
|
+
export function isRetryableHttpStatus(status) {
|
|
39
|
+
return RETRYABLE_HTTP_STATUSES.has(status);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Equal-jitter exponential backoff: attempt 1 → 0.5-1s, 2 → 1-2s, 3 → 2-4s,
|
|
43
|
+
* 4 → 4-8s, capped at 32s. A retry-after hint from the server wins (capped
|
|
44
|
+
* at 60s) since it reflects actual load shedding.
|
|
45
|
+
*/
|
|
46
|
+
export function computeRetryDelayMs(attempt, options) {
|
|
47
|
+
if (process.env.NODE_ENV === "test")
|
|
48
|
+
return 0;
|
|
49
|
+
if (options?.retryAfterMs !== undefined && options.retryAfterMs >= 0) {
|
|
50
|
+
return Math.min(options.retryAfterMs, MAX_RETRY_AFTER_MS);
|
|
51
|
+
}
|
|
52
|
+
const cap = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** Math.max(0, attempt - 1));
|
|
53
|
+
return Math.floor(cap / 2 + Math.random() * (cap / 2));
|
|
54
|
+
}
|
|
55
|
+
export function retryAfterMsFromResponse(response) {
|
|
56
|
+
const header = response.headers.get("retry-after")?.trim();
|
|
57
|
+
if (!header)
|
|
58
|
+
return undefined;
|
|
59
|
+
const seconds = Number(header);
|
|
60
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
61
|
+
return Math.round(seconds * 1000);
|
|
62
|
+
const date = Date.parse(header);
|
|
63
|
+
if (!Number.isNaN(date))
|
|
64
|
+
return Math.max(0, date - Date.now());
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
export function sleepBeforeRetry(ms, signal) {
|
|
68
|
+
if (signal?.aborted)
|
|
69
|
+
return Promise.reject(toAbortError(signal));
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const onAbort = () => {
|
|
72
|
+
clearTimeout(timeout);
|
|
73
|
+
reject(toAbortError(signal));
|
|
74
|
+
};
|
|
75
|
+
const timeout = setTimeout(() => {
|
|
76
|
+
signal?.removeEventListener("abort", onAbort);
|
|
77
|
+
resolve();
|
|
78
|
+
}, ms);
|
|
79
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function toAbortError(signal) {
|
|
83
|
+
if (signal?.reason instanceof Error)
|
|
84
|
+
return signal.reason;
|
|
85
|
+
const error = new Error(typeof signal?.reason === "string" ? signal.reason : "Request retry aborted.");
|
|
86
|
+
error.name = "AbortError";
|
|
87
|
+
return error;
|
|
88
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface SystemProxySettings {
|
|
2
|
+
httpProxy?: string;
|
|
3
|
+
httpsProxy?: string;
|
|
4
|
+
exceptions: string[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Resolves the OS-level proxy for a request URL. Used as a fallback when no
|
|
8
|
+
* proxy environment variables are set, so Bubble follows the same proxy that
|
|
9
|
+
* browsers and other GUI apps use (e.g. Clash/Surge "system proxy" mode).
|
|
10
|
+
* Reads `scutil --proxy` on macOS and the Internet Settings registry key on
|
|
11
|
+
* Windows. Returns undefined on other platforms, when disabled via
|
|
12
|
+
* BUBBLE_SYSTEM_PROXY=0, or when the URL matches the OS proxy bypass list.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getSystemProxyForUrl(url: URL | undefined, env?: NodeJS.ProcessEnv): string | undefined;
|
|
15
|
+
export declare function systemProxyForUrl(url: URL, settings: SystemProxySettings): string | undefined;
|
|
16
|
+
export declare function parseScutilProxyOutput(output: string): SystemProxySettings | undefined;
|
|
17
|
+
export declare function parseWindowsProxyOutput(output: string): SystemProxySettings | undefined;
|
|
18
|
+
export declare function resetSystemProxyCacheForTests(): void;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
const CACHE_TTL_MS = 30_000;
|
|
3
|
+
const FALSE_VALUES = new Set(["0", "false", "no", "off"]);
|
|
4
|
+
let cache;
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the OS-level proxy for a request URL. Used as a fallback when no
|
|
7
|
+
* proxy environment variables are set, so Bubble follows the same proxy that
|
|
8
|
+
* browsers and other GUI apps use (e.g. Clash/Surge "system proxy" mode).
|
|
9
|
+
* Reads `scutil --proxy` on macOS and the Internet Settings registry key on
|
|
10
|
+
* Windows. Returns undefined on other platforms, when disabled via
|
|
11
|
+
* BUBBLE_SYSTEM_PROXY=0, or when the URL matches the OS proxy bypass list.
|
|
12
|
+
*/
|
|
13
|
+
export function getSystemProxyForUrl(url, env = process.env) {
|
|
14
|
+
if (!url || isSystemProxyDisabled(env))
|
|
15
|
+
return undefined;
|
|
16
|
+
const settings = readSystemProxySettings();
|
|
17
|
+
if (!settings)
|
|
18
|
+
return undefined;
|
|
19
|
+
return systemProxyForUrl(url, settings);
|
|
20
|
+
}
|
|
21
|
+
export function systemProxyForUrl(url, settings) {
|
|
22
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
23
|
+
return undefined;
|
|
24
|
+
const hostname = url.hostname.toLowerCase();
|
|
25
|
+
if (isLoopbackHostname(hostname))
|
|
26
|
+
return undefined;
|
|
27
|
+
if (settings.exceptions.some((entry) => systemExceptionMatches(entry, hostname)))
|
|
28
|
+
return undefined;
|
|
29
|
+
if (url.protocol === "https:")
|
|
30
|
+
return settings.httpsProxy ?? settings.httpProxy;
|
|
31
|
+
return settings.httpProxy ?? settings.httpsProxy;
|
|
32
|
+
}
|
|
33
|
+
export function parseScutilProxyOutput(output) {
|
|
34
|
+
const settings = {
|
|
35
|
+
httpProxy: proxyUrlFromKeys(output, "HTTP"),
|
|
36
|
+
httpsProxy: proxyUrlFromKeys(output, "HTTPS"),
|
|
37
|
+
exceptions: parseExceptionsList(output),
|
|
38
|
+
};
|
|
39
|
+
if (!settings.httpProxy && !settings.httpsProxy)
|
|
40
|
+
return undefined;
|
|
41
|
+
return settings;
|
|
42
|
+
}
|
|
43
|
+
export function parseWindowsProxyOutput(output) {
|
|
44
|
+
const enable = readRegistryValue(output, "ProxyEnable", "REG_DWORD");
|
|
45
|
+
if (!enable || Number.parseInt(enable, 16) !== 1)
|
|
46
|
+
return undefined;
|
|
47
|
+
const server = readRegistryValue(output, "ProxyServer", "REG_SZ");
|
|
48
|
+
if (!server)
|
|
49
|
+
return undefined;
|
|
50
|
+
const { httpProxy, httpsProxy } = parseWindowsProxyServer(server);
|
|
51
|
+
if (!httpProxy && !httpsProxy)
|
|
52
|
+
return undefined;
|
|
53
|
+
const override = readRegistryValue(output, "ProxyOverride", "REG_SZ") ?? "";
|
|
54
|
+
const exceptions = override
|
|
55
|
+
.split(";")
|
|
56
|
+
.map((item) => item.trim().toLowerCase())
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
return { httpProxy, httpsProxy, exceptions };
|
|
59
|
+
}
|
|
60
|
+
// ProxyServer is either "host:port" (all protocols) or per-protocol
|
|
61
|
+
// "http=host:port;https=host:port;ftp=...;socks=..." — ftp/socks are ignored.
|
|
62
|
+
function parseWindowsProxyServer(value) {
|
|
63
|
+
if (!value.includes("=")) {
|
|
64
|
+
const url = normalizeProxyHostPort(value);
|
|
65
|
+
return { httpProxy: url, httpsProxy: url };
|
|
66
|
+
}
|
|
67
|
+
const result = {};
|
|
68
|
+
for (const part of value.split(";")) {
|
|
69
|
+
const [protocol, hostPort] = part.split("=").map((item) => item.trim());
|
|
70
|
+
if (!protocol || !hostPort)
|
|
71
|
+
continue;
|
|
72
|
+
const url = normalizeProxyHostPort(hostPort);
|
|
73
|
+
if (!url)
|
|
74
|
+
continue;
|
|
75
|
+
if (protocol.toLowerCase() === "http")
|
|
76
|
+
result.httpProxy = url;
|
|
77
|
+
if (protocol.toLowerCase() === "https")
|
|
78
|
+
result.httpsProxy = url;
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
function normalizeProxyHostPort(value) {
|
|
83
|
+
const trimmed = value.trim().replace(/^https?:\/\//i, "");
|
|
84
|
+
const match = trimmed.match(/^([^\s:]+):(\d+)$/);
|
|
85
|
+
if (!match)
|
|
86
|
+
return undefined;
|
|
87
|
+
const port = Number(match[2]);
|
|
88
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535)
|
|
89
|
+
return undefined;
|
|
90
|
+
return `http://${match[1]}:${port}`;
|
|
91
|
+
}
|
|
92
|
+
function readRegistryValue(output, key, type) {
|
|
93
|
+
const match = output.match(new RegExp(`^\\s*${key}\\s+${type}\\s+(.+?)\\s*$`, "m"));
|
|
94
|
+
return match?.[1];
|
|
95
|
+
}
|
|
96
|
+
export function resetSystemProxyCacheForTests() {
|
|
97
|
+
cache = undefined;
|
|
98
|
+
}
|
|
99
|
+
function isSystemProxyDisabled(env) {
|
|
100
|
+
const value = env.BUBBLE_SYSTEM_PROXY?.trim().toLowerCase();
|
|
101
|
+
return !!value && FALSE_VALUES.has(value);
|
|
102
|
+
}
|
|
103
|
+
function readSystemProxySettings() {
|
|
104
|
+
if (process.platform !== "darwin" && process.platform !== "win32")
|
|
105
|
+
return undefined;
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
if (cache && now - cache.at < CACHE_TTL_MS)
|
|
108
|
+
return cache.settings;
|
|
109
|
+
let settings;
|
|
110
|
+
try {
|
|
111
|
+
settings = process.platform === "darwin" ? readMacProxySettings() : readWindowsProxySettings();
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
settings = undefined;
|
|
115
|
+
}
|
|
116
|
+
cache = { at: now, settings };
|
|
117
|
+
return settings;
|
|
118
|
+
}
|
|
119
|
+
function readMacProxySettings() {
|
|
120
|
+
const output = execFileSync("scutil", ["--proxy"], { encoding: "utf-8", timeout: 2000 });
|
|
121
|
+
return parseScutilProxyOutput(output);
|
|
122
|
+
}
|
|
123
|
+
function readWindowsProxySettings() {
|
|
124
|
+
const output = execFileSync("reg", ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"], { encoding: "utf-8", timeout: 2000, windowsHide: true });
|
|
125
|
+
return parseWindowsProxyOutput(output);
|
|
126
|
+
}
|
|
127
|
+
function proxyUrlFromKeys(output, prefix) {
|
|
128
|
+
if (readScalar(output, `${prefix}Enable`) !== "1")
|
|
129
|
+
return undefined;
|
|
130
|
+
const host = readScalar(output, `${prefix}Proxy`);
|
|
131
|
+
const port = Number(readScalar(output, `${prefix}Port`));
|
|
132
|
+
if (!host || !Number.isInteger(port) || port <= 0 || port > 65535)
|
|
133
|
+
return undefined;
|
|
134
|
+
return `http://${host}:${port}`;
|
|
135
|
+
}
|
|
136
|
+
function readScalar(output, key) {
|
|
137
|
+
const match = output.match(new RegExp(`^\\s*${key}\\s*:\\s*(\\S+)`, "m"));
|
|
138
|
+
return match?.[1];
|
|
139
|
+
}
|
|
140
|
+
function parseExceptionsList(output) {
|
|
141
|
+
const match = output.match(/ExceptionsList\s*:\s*<array>\s*\{([\s\S]*?)\}/);
|
|
142
|
+
if (!match)
|
|
143
|
+
return [];
|
|
144
|
+
return [...match[1].matchAll(/^\s*\d+\s*:\s*(\S+)/gm)].map((item) => item[1].toLowerCase());
|
|
145
|
+
}
|
|
146
|
+
function systemExceptionMatches(entry, hostname) {
|
|
147
|
+
if (entry === "<local>")
|
|
148
|
+
return !hostname.includes(".");
|
|
149
|
+
// CIDR entries (e.g. 192.168.0.0/16) would need the resolved IP to match;
|
|
150
|
+
// skip them so we never wrongly bypass the proxy for public hostnames.
|
|
151
|
+
if (entry.includes("/"))
|
|
152
|
+
return false;
|
|
153
|
+
if (entry.startsWith("*."))
|
|
154
|
+
return hostname === entry.slice(2) || hostname.endsWith(entry.slice(1));
|
|
155
|
+
// Windows ProxyOverride allows wildcards anywhere, e.g. 127.* or 192.168.*
|
|
156
|
+
if (entry.includes("*"))
|
|
157
|
+
return wildcardMatches(entry, hostname);
|
|
158
|
+
if (entry.startsWith("."))
|
|
159
|
+
return hostname.endsWith(entry);
|
|
160
|
+
return entry === hostname;
|
|
161
|
+
}
|
|
162
|
+
function wildcardMatches(pattern, hostname) {
|
|
163
|
+
const regex = new RegExp(`^${pattern.split("*").map(escapeRegExp).join(".*")}$`);
|
|
164
|
+
return regex.test(hostname);
|
|
165
|
+
}
|
|
166
|
+
function escapeRegExp(value) {
|
|
167
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
168
|
+
}
|
|
169
|
+
function isLoopbackHostname(hostname) {
|
|
170
|
+
return hostname === "localhost"
|
|
171
|
+
|| hostname === "::1"
|
|
172
|
+
|| hostname === "[::1]"
|
|
173
|
+
|| hostname.startsWith("127.")
|
|
174
|
+
|| hostname.endsWith(".localhost");
|
|
175
|
+
}
|
|
@@ -79,6 +79,7 @@ export declare function buildAnthropicRequest(options: AnthropicProviderOptions,
|
|
|
79
79
|
thinkingLevel?: ThinkingLevel;
|
|
80
80
|
stream?: boolean;
|
|
81
81
|
}): AnthropicRequest;
|
|
82
|
+
export declare function resolveAnthropicMaxTokens(options: AnthropicProviderOptions, model: string): number;
|
|
82
83
|
export declare function supportsAnthropicPromptCache(options: AnthropicProviderOptions, model: string): boolean;
|
|
83
84
|
export declare function toAnthropicMessages(messages: ProviderMessage[], echoThinking?: boolean): {
|
|
84
85
|
system: string;
|