@bubblebrain-ai/bubble 0.0.19 → 0.0.21
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 +10 -0
- package/dist/agent.js +310 -18
- package/dist/approval/controller.d.ts +6 -0
- package/dist/approval/controller.js +104 -11
- package/dist/checkpoints.d.ts +57 -0
- package/dist/checkpoints.js +0 -0
- package/dist/debug-trace.js +4 -0
- package/dist/feishu/agent-host/run-driver.js +29 -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 +86 -13
- 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.d.ts +0 -1
- package/dist/network/chatgpt-transport.js +40 -121
- 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/session.d.ts +31 -0
- package/dist/session.js +69 -0
- package/dist/slash-commands/commands.js +164 -0
- package/dist/slash-commands/types.d.ts +6 -0
- package/dist/tools/bash.js +4 -0
- package/dist/tools/edit-apply.js +63 -3
- package/dist/tools/edit.d.ts +2 -1
- package/dist/tools/edit.js +6 -5
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.js +2 -2
- package/dist/tools/write.d.ts +2 -1
- package/dist/tools/write.js +2 -1
- 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/image-paste.d.ts +18 -0
- package/dist/tui/image-paste.js +60 -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 +568 -223
- package/dist/tui/trace-groups.d.ts +16 -0
- package/dist/tui/trace-groups.js +82 -5
- package/dist/tui/transcript-scroll.d.ts +25 -0
- package/dist/tui/transcript-scroll.js +20 -0
- package/dist/tui/wordmark.d.ts +1 -0
- package/dist/tui/wordmark.js +56 -54
- package/dist/tui-ink/app.d.ts +4 -1
- package/dist/tui-ink/app.js +303 -248
- package/dist/tui-ink/display-history.d.ts +16 -1
- package/dist/tui-ink/display-history.js +50 -21
- package/dist/tui-ink/footer.d.ts +6 -12
- package/dist/tui-ink/footer.js +10 -29
- package/dist/tui-ink/image-paste.d.ts +59 -0
- package/dist/tui-ink/image-paste.js +277 -0
- package/dist/tui-ink/input-box.d.ts +26 -1
- package/dist/tui-ink/input-box.js +171 -41
- package/dist/tui-ink/message-list.d.ts +1 -1
- package/dist/tui-ink/message-list.js +46 -29
- package/dist/tui-ink/run.d.ts +7 -2
- package/dist/tui-ink/run.js +73 -23
- package/dist/tui-ink/terminal-mouse.d.ts +1 -0
- package/dist/tui-ink/terminal-mouse.js +4 -0
- package/dist/tui-ink/trace-groups.d.ts +16 -0
- package/dist/tui-ink/trace-groups.js +90 -6
- package/dist/tui-ink/transcript-viewport-math.d.ts +11 -0
- package/dist/tui-ink/transcript-viewport-math.js +17 -0
- package/dist/tui-ink/transcript-viewport.d.ts +24 -0
- package/dist/tui-ink/transcript-viewport.js +83 -0
- package/dist/tui-ink/welcome.d.ts +9 -7
- package/dist/tui-ink/welcome.js +7 -33
- 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,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;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { getAvailableThinkingLevels, normalizeThinkingLevel } from "./provider-transform.js";
|
|
2
|
+
import { isProviderTransportError, normalizeProviderNetworkError, providerFetch } from "./network/provider-transport.js";
|
|
3
|
+
import { computeRetryDelayMs, getProviderMaxRetries, isRetryableHttpStatus, ProviderStreamInterruptedError, retryAfterMsFromResponse, sleepBeforeRetry, } from "./network/retry.js";
|
|
2
4
|
const ANTHROPIC_VERSION = "2023-06-01";
|
|
3
5
|
const DEFAULT_MAX_TOKENS = 8192;
|
|
6
|
+
const ANTHROPIC_OPUS_LONG_OUTPUT_MAX_TOKENS = 128000;
|
|
7
|
+
const ANTHROPIC_LONG_OUTPUT_MAX_TOKENS = 64000;
|
|
4
8
|
const ANTHROPIC_PROMPT_CACHE_CONTROL = { type: "ephemeral" };
|
|
5
9
|
const MINIMAX_PROMPT_CACHE_MODELS = new Set([
|
|
6
10
|
"minimax-m2.7",
|
|
@@ -22,14 +26,14 @@ export function createAnthropicMessagesProvider(options) {
|
|
|
22
26
|
thinkingLevel: chatOptions.thinkingLevel,
|
|
23
27
|
stream: true,
|
|
24
28
|
});
|
|
25
|
-
const
|
|
29
|
+
const events = streamAnthropicEventsWithRetry(options, {
|
|
26
30
|
url: resolveAnthropicMessagesUrl(options.baseURL),
|
|
27
31
|
stream: true,
|
|
28
32
|
method: "POST",
|
|
29
33
|
body: JSON.stringify(body),
|
|
30
34
|
signal: chatOptions.abortSignal,
|
|
31
35
|
});
|
|
32
|
-
yield* translateAnthropicStream(
|
|
36
|
+
yield* translateAnthropicStream(events);
|
|
33
37
|
yield { type: "done" };
|
|
34
38
|
}
|
|
35
39
|
async function complete(messages, chatOptions) {
|
|
@@ -65,24 +69,37 @@ export function buildAnthropicRequest(options, messages, chatOptions) {
|
|
|
65
69
|
cache_control: ANTHROPIC_PROMPT_CACHE_CONTROL,
|
|
66
70
|
};
|
|
67
71
|
}
|
|
72
|
+
const effectiveThinkingLevel = normalizeThinkingLevel(chatOptions.thinkingLevel ?? options.thinkingLevel ?? "off", getAvailableThinkingLevels(options.providerId || "", chatOptions.model));
|
|
68
73
|
const body = {
|
|
69
74
|
model: chatOptions.model,
|
|
70
|
-
max_tokens:
|
|
75
|
+
max_tokens: resolveAnthropicMaxTokens(options, chatOptions.model),
|
|
71
76
|
system: buildAnthropicSystem(system, enablePromptCache),
|
|
72
77
|
messages: anthropicMessages,
|
|
73
78
|
tools: tools && tools.length > 0 ? tools : undefined,
|
|
74
79
|
tool_choice: tools && tools.length > 0 ? { type: chatOptions.toolChoice ?? "auto" } : undefined,
|
|
75
80
|
stream: chatOptions.stream || undefined,
|
|
76
81
|
};
|
|
77
|
-
if (typeof chatOptions.temperature === "number"
|
|
82
|
+
if (typeof chatOptions.temperature === "number"
|
|
83
|
+
&& shouldSendTemperature(options, chatOptions.model, effectiveThinkingLevel)) {
|
|
78
84
|
body.temperature = chatOptions.temperature;
|
|
79
85
|
}
|
|
80
|
-
const effectiveThinkingLevel = normalizeThinkingLevel(chatOptions.thinkingLevel ?? options.thinkingLevel ?? "off", getAvailableThinkingLevels(options.providerId || "", chatOptions.model));
|
|
81
86
|
if (effectiveThinkingLevel !== "off") {
|
|
82
87
|
body.thinking = { type: "adaptive" };
|
|
83
88
|
}
|
|
84
89
|
return body;
|
|
85
90
|
}
|
|
91
|
+
export function resolveAnthropicMaxTokens(options, model) {
|
|
92
|
+
if (!isOfficialAnthropicBaseUrl(options.baseURL)) {
|
|
93
|
+
return DEFAULT_MAX_TOKENS;
|
|
94
|
+
}
|
|
95
|
+
if (isFableModelWith128kOutput(model) || isOpusModelWith128kOutput(model)) {
|
|
96
|
+
return ANTHROPIC_OPUS_LONG_OUTPUT_MAX_TOKENS;
|
|
97
|
+
}
|
|
98
|
+
if (isSonnetOrHaikuModelWith64kOutput(model)) {
|
|
99
|
+
return ANTHROPIC_LONG_OUTPUT_MAX_TOKENS;
|
|
100
|
+
}
|
|
101
|
+
return DEFAULT_MAX_TOKENS;
|
|
102
|
+
}
|
|
86
103
|
function buildAnthropicSystem(system, enablePromptCache) {
|
|
87
104
|
if (!system)
|
|
88
105
|
return undefined;
|
|
@@ -352,27 +369,71 @@ export async function* readSseEvents(response) {
|
|
|
352
369
|
reader.releaseLock();
|
|
353
370
|
}
|
|
354
371
|
}
|
|
372
|
+
async function* streamAnthropicEventsWithRetry(options, request) {
|
|
373
|
+
const maxRetries = getProviderMaxRetries();
|
|
374
|
+
for (let attempt = 0;; attempt++) {
|
|
375
|
+
// Connection-level failures and retryable HTTP statuses are retried
|
|
376
|
+
// inside fetchAnthropicResponseWithRetry; an error thrown from it has
|
|
377
|
+
// already exhausted its budget, so it propagates without another loop.
|
|
378
|
+
const response = await fetchAnthropicResponseWithRetry(options, request);
|
|
379
|
+
let sawSseEvent = false;
|
|
380
|
+
try {
|
|
381
|
+
for await (const event of readSseEvents(response)) {
|
|
382
|
+
sawSseEvent = true;
|
|
383
|
+
yield event;
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
const normalized = normalizeAnthropicTransportError(error, request.url);
|
|
389
|
+
if (sawSseEvent) {
|
|
390
|
+
// Partial content already surfaced — only the agent loop can discard
|
|
391
|
+
// the half-built assistant message and safely re-issue the request.
|
|
392
|
+
if (!request.signal?.aborted && isProviderTransportError(error)) {
|
|
393
|
+
throw new ProviderStreamInterruptedError(normalized.message, { cause: normalized });
|
|
394
|
+
}
|
|
395
|
+
throw normalized;
|
|
396
|
+
}
|
|
397
|
+
if (request.signal?.aborted || attempt >= maxRetries || !isProviderTransportError(error)) {
|
|
398
|
+
throw normalized;
|
|
399
|
+
}
|
|
400
|
+
await sleepBeforeRetry(computeRetryDelayMs(attempt + 1), request.signal);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
355
404
|
async function fetchAnthropicResponseWithRetry(options, request) {
|
|
356
|
-
const
|
|
357
|
-
let
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
405
|
+
const maxRetries = getProviderMaxRetries();
|
|
406
|
+
for (let attempt = 0;; attempt++) {
|
|
407
|
+
let response;
|
|
408
|
+
try {
|
|
409
|
+
response = await providerFetch(request.url, {
|
|
410
|
+
method: request.method,
|
|
411
|
+
headers: buildAnthropicHeaders(options, request.stream),
|
|
412
|
+
body: request.body,
|
|
413
|
+
signal: request.signal,
|
|
414
|
+
keepalive: false,
|
|
415
|
+
}, {
|
|
416
|
+
providerName: "Anthropic",
|
|
417
|
+
verboseEnvVar: "BUBBLE_ANTHROPIC_FETCH_VERBOSE",
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
// No response received, so the request is safe to re-issue.
|
|
422
|
+
if (request.signal?.aborted || attempt >= maxRetries || !isProviderTransportError(error)) {
|
|
423
|
+
throw normalizeAnthropicTransportError(error, request.url);
|
|
424
|
+
}
|
|
425
|
+
await sleepBeforeRetry(computeRetryDelayMs(attempt + 1), request.signal);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
365
428
|
if (response.ok)
|
|
366
429
|
return response;
|
|
367
430
|
const detail = await readAnthropicErrorDetail(response);
|
|
368
431
|
const error = new Error(`Anthropic Messages API error ${response.status}: ${detail || response.statusText}`);
|
|
369
|
-
|
|
370
|
-
if (attempt >= maxAttempts || !isRetryableMiniMaxAnthropicError(response.status, detail)) {
|
|
432
|
+
if (request.signal?.aborted || attempt >= maxRetries || !isRetryableAnthropicHttpError(response.status, detail)) {
|
|
371
433
|
throw error;
|
|
372
434
|
}
|
|
373
|
-
await sleepBeforeRetry(
|
|
435
|
+
await sleepBeforeRetry(computeRetryDelayMs(attempt + 1, { retryAfterMs: retryAfterMsFromResponse(response) }), request.signal);
|
|
374
436
|
}
|
|
375
|
-
throw lastError ?? new Error("Anthropic Messages API request failed");
|
|
376
437
|
}
|
|
377
438
|
function resolveAnthropicMessagesUrl(baseURL) {
|
|
378
439
|
const normalized = baseURL.trim().replace(/\/+$/, "");
|
|
@@ -403,42 +464,18 @@ async function readAnthropicErrorDetail(response) {
|
|
|
403
464
|
return response.statusText;
|
|
404
465
|
}
|
|
405
466
|
}
|
|
406
|
-
function
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|| status === 502
|
|
414
|
-
|| status === 503
|
|
415
|
-
|| status === 504
|
|
416
|
-
|| detail.includes("714 (1000)");
|
|
417
|
-
}
|
|
418
|
-
function getAnthropicRetryDelayMs() {
|
|
419
|
-
if (process.env.NODE_ENV === "test")
|
|
420
|
-
return 0;
|
|
421
|
-
return 800 + Math.floor(Math.random() * 700);
|
|
422
|
-
}
|
|
423
|
-
function sleepBeforeRetry(ms, signal) {
|
|
424
|
-
if (signal?.aborted) {
|
|
425
|
-
return Promise.reject(toAbortError(signal));
|
|
426
|
-
}
|
|
427
|
-
return new Promise((resolve, reject) => {
|
|
428
|
-
const timeout = setTimeout(resolve, ms);
|
|
429
|
-
signal?.addEventListener("abort", () => {
|
|
430
|
-
clearTimeout(timeout);
|
|
431
|
-
reject(toAbortError(signal));
|
|
432
|
-
}, { once: true });
|
|
467
|
+
function normalizeAnthropicTransportError(error, url) {
|
|
468
|
+
if (!isProviderTransportError(error)) {
|
|
469
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
470
|
+
}
|
|
471
|
+
return normalizeProviderNetworkError(error, {
|
|
472
|
+
providerName: "Anthropic",
|
|
473
|
+
input: url,
|
|
433
474
|
});
|
|
434
475
|
}
|
|
435
|
-
function
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
return reason;
|
|
439
|
-
const error = new Error("Anthropic request retry aborted.");
|
|
440
|
-
error.name = "AbortError";
|
|
441
|
-
return error;
|
|
476
|
+
function isRetryableAnthropicHttpError(status, detail) {
|
|
477
|
+
// "714 (1000)" is a transient MiniMax backend error surfaced as a 500.
|
|
478
|
+
return isRetryableHttpStatus(status) || detail.includes("714 (1000)");
|
|
442
479
|
}
|
|
443
480
|
function parseSseEvent(raw) {
|
|
444
481
|
const dataLines = [];
|
|
@@ -573,6 +610,44 @@ function shouldEchoThinking(providerId) {
|
|
|
573
610
|
function shouldSendBearerAuth(options) {
|
|
574
611
|
return !isOfficialAnthropicBaseUrl(options.baseURL) || options.providerId?.startsWith("minimax") === true;
|
|
575
612
|
}
|
|
613
|
+
function shouldSendTemperature(options, model, thinkingLevel) {
|
|
614
|
+
if (!isOfficialAnthropicBaseUrl(options.baseURL))
|
|
615
|
+
return true;
|
|
616
|
+
if (thinkingLevel !== "off")
|
|
617
|
+
return false;
|
|
618
|
+
return !isOpusModelWithoutSamplingControls(model);
|
|
619
|
+
}
|
|
620
|
+
function isOpusModelWith128kOutput(model) {
|
|
621
|
+
return isClaudeFamilyVersionAtLeast(model, "opus", 4, 6);
|
|
622
|
+
}
|
|
623
|
+
function isFableModelWith128kOutput(model) {
|
|
624
|
+
return model.toLowerCase().startsWith("claude-fable-5");
|
|
625
|
+
}
|
|
626
|
+
function isSonnetOrHaikuModelWith64kOutput(model) {
|
|
627
|
+
const normalized = model.toLowerCase();
|
|
628
|
+
return normalized.startsWith("claude-sonnet-4-6")
|
|
629
|
+
|| normalized.startsWith("claude-haiku-4-5");
|
|
630
|
+
}
|
|
631
|
+
function isOpusModelWithoutSamplingControls(model) {
|
|
632
|
+
return isClaudeFamilyVersionAtLeast(model, "opus", 4, 7);
|
|
633
|
+
}
|
|
634
|
+
function isClaudeFamilyVersionAtLeast(model, family, minMajor, minMinor) {
|
|
635
|
+
const normalized = model.toLowerCase();
|
|
636
|
+
if (!normalized.startsWith(`claude-${family}-`))
|
|
637
|
+
return false;
|
|
638
|
+
const [, , majorSegment, minorSegment] = normalized.split("-");
|
|
639
|
+
const major = Number(majorSegment);
|
|
640
|
+
if (!Number.isFinite(major))
|
|
641
|
+
return false;
|
|
642
|
+
if (major > minMajor)
|
|
643
|
+
return true;
|
|
644
|
+
if (major < minMajor)
|
|
645
|
+
return false;
|
|
646
|
+
if (!minorSegment || minorSegment.length > 2)
|
|
647
|
+
return false;
|
|
648
|
+
const minor = Number(minorSegment);
|
|
649
|
+
return Number.isFinite(minor) && minor >= minMinor;
|
|
650
|
+
}
|
|
576
651
|
function isMiniMaxAnthropicEndpoint(options) {
|
|
577
652
|
const providerId = (options.providerId ?? "").toLowerCase();
|
|
578
653
|
if (providerId !== "minimax" && providerId !== "minimax-anthropic")
|
|
@@ -2,11 +2,10 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { listBuiltinModels } from "./model-catalog.js";
|
|
3
3
|
import { resolveProviderRequestConfig } from "./provider-transform.js";
|
|
4
4
|
import { chatGptFetch } from "./network/chatgpt-transport.js";
|
|
5
|
+
import { computeRetryDelayMs, getProviderMaxRetries, isRetryableHttpStatus, ProviderStreamInterruptedError, sleepBeforeRetry, } from "./network/retry.js";
|
|
5
6
|
const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
6
7
|
const OPENAI_BETA_RESPONSES = "responses=experimental";
|
|
7
8
|
const TOKEN_REFRESH_GRACE_MS = 5 * 60 * 1000;
|
|
8
|
-
const CODEX_TRANSPORT_MAX_RETRIES = 2;
|
|
9
|
-
const CODEX_TRANSPORT_RETRY_BASE_DELAY_MS = 250;
|
|
10
9
|
// OpenAI gates new codex models server-side by client_version (each model carries a
|
|
11
10
|
// `minimal_client_version`). Track a recent real Codex CLI release; override via env
|
|
12
11
|
// when OpenAI lifts the gate again before we cut a new release.
|
|
@@ -221,6 +220,13 @@ export function createOpenAICodexProvider(options) {
|
|
|
221
220
|
return;
|
|
222
221
|
}
|
|
223
222
|
catch (error) {
|
|
223
|
+
if (sawParsedSseEvent
|
|
224
|
+
&& !chatOptions.abortSignal?.aborted
|
|
225
|
+
&& isTransientCodexTransportError(error)) {
|
|
226
|
+
// Partial content already surfaced — the agent loop discards the
|
|
227
|
+
// half-built assistant message and re-issues the whole request.
|
|
228
|
+
throw new ProviderStreamInterruptedError(error instanceof Error ? error.message : String(error), { cause: error });
|
|
229
|
+
}
|
|
224
230
|
if (!shouldRetryCodexTransportError({
|
|
225
231
|
error,
|
|
226
232
|
attempt,
|
|
@@ -229,7 +235,7 @@ export function createOpenAICodexProvider(options) {
|
|
|
229
235
|
})) {
|
|
230
236
|
throw error;
|
|
231
237
|
}
|
|
232
|
-
await
|
|
238
|
+
await sleepBeforeRetry(computeRetryDelayMs(attempt + 1), chatOptions.abortSignal);
|
|
233
239
|
}
|
|
234
240
|
}
|
|
235
241
|
}
|
|
@@ -439,9 +445,17 @@ function shouldRetryCodexTransportError(input) {
|
|
|
439
445
|
return false;
|
|
440
446
|
if (input.sawParsedSseEvent)
|
|
441
447
|
return false;
|
|
442
|
-
if (input.attempt >=
|
|
448
|
+
if (input.attempt >= getProviderMaxRetries())
|
|
449
|
+
return false;
|
|
450
|
+
return isTransientCodexTransportError(input.error) || isRetryableCodexHttpError(input.error);
|
|
451
|
+
}
|
|
452
|
+
function isRetryableCodexHttpError(error) {
|
|
453
|
+
if (!(error instanceof Error))
|
|
454
|
+
return false;
|
|
455
|
+
const match = error.message.match(/^(\d{3}) status code/);
|
|
456
|
+
if (!match)
|
|
443
457
|
return false;
|
|
444
|
-
return
|
|
458
|
+
return isRetryableHttpStatus(Number(match[1]));
|
|
445
459
|
}
|
|
446
460
|
function isTransientCodexTransportError(error) {
|
|
447
461
|
const text = errorMessageChain(error).join("\n");
|
|
@@ -487,30 +501,6 @@ function errorMessageChain(error) {
|
|
|
487
501
|
}
|
|
488
502
|
return messages;
|
|
489
503
|
}
|
|
490
|
-
function codexRetryDelayMs(attempt) {
|
|
491
|
-
return CODEX_TRANSPORT_RETRY_BASE_DELAY_MS * Math.pow(3, attempt);
|
|
492
|
-
}
|
|
493
|
-
function sleepBeforeCodexRetry(ms, signal) {
|
|
494
|
-
if (signal?.aborted)
|
|
495
|
-
return Promise.reject(toAbortError(signal));
|
|
496
|
-
return new Promise((resolve, reject) => {
|
|
497
|
-
const onAbort = () => {
|
|
498
|
-
clearTimeout(timeout);
|
|
499
|
-
signal?.removeEventListener("abort", onAbort);
|
|
500
|
-
reject(toAbortError(signal));
|
|
501
|
-
};
|
|
502
|
-
const timeout = setTimeout(() => {
|
|
503
|
-
signal?.removeEventListener("abort", onAbort);
|
|
504
|
-
resolve();
|
|
505
|
-
}, ms);
|
|
506
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
function toAbortError(signal) {
|
|
510
|
-
if (signal?.reason instanceof Error)
|
|
511
|
-
return signal.reason;
|
|
512
|
-
return new DOMException(typeof signal?.reason === "string" ? signal.reason : "Aborted", "AbortError");
|
|
513
|
-
}
|
|
514
504
|
function buildBaseHeaders(accessToken, accountId, sessionId, extraHeaders) {
|
|
515
505
|
const headers = new Headers(extraHeaders);
|
|
516
506
|
headers.set("Authorization", `Bearer ${accessToken}`);
|
package/dist/session-log.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sanitizeAssistantProviderMetadata, sanitizeInternalReminderBlocks } from "./agent/internal-reminder-sanitizer.js";
|
|
1
|
+
import { sanitizeAssistantProviderMetadata, sanitizeInternalReasoningText, sanitizeInternalReminderBlocks, } from "./agent/internal-reminder-sanitizer.js";
|
|
2
2
|
export class SessionLog {
|
|
3
3
|
entries = [];
|
|
4
4
|
load(lines) {
|
|
@@ -130,7 +130,7 @@ export class SessionLog {
|
|
|
130
130
|
role: "assistant",
|
|
131
131
|
content: sanitizeInternalReminderBlocks(entry.message.content),
|
|
132
132
|
reasoning: entry.message.reasoning !== undefined
|
|
133
|
-
?
|
|
133
|
+
? sanitizeInternalReasoningText(entry.message.reasoning)
|
|
134
134
|
: undefined,
|
|
135
135
|
providerMetadata: sanitizeAssistantProviderMetadata(cloneProviderMetadata(entry.message.providerMetadata)),
|
|
136
136
|
});
|
|
@@ -197,7 +197,7 @@ function normalizeMessageToEntries(message, id, timestamp) {
|
|
|
197
197
|
role: "assistant",
|
|
198
198
|
content: sanitizeInternalReminderBlocks(message.content),
|
|
199
199
|
reasoning: message.reasoning !== undefined
|
|
200
|
-
?
|
|
200
|
+
? sanitizeInternalReasoningText(message.reasoning)
|
|
201
201
|
: undefined,
|
|
202
202
|
model: message.model,
|
|
203
203
|
providerId: message.providerId,
|
package/dist/session.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session Manager - Append-only JSONL persistence over a structured session log.
|
|
3
3
|
*/
|
|
4
|
+
import { CheckpointStore } from "./checkpoints.js";
|
|
4
5
|
import { type CompactOptions, type CompactResult } from "./context/compact.js";
|
|
5
6
|
import type { Message, Todo } from "./types.js";
|
|
6
7
|
import type { SessionLogEntry, SessionMarkerKind, SessionMetadata } from "./session-types.js";
|
|
@@ -16,9 +17,25 @@ export interface SessionSummary {
|
|
|
16
17
|
mtime: number;
|
|
17
18
|
}
|
|
18
19
|
export type { SessionLogEntry, SessionMarkerKind, SessionMetadata } from "./session-types.js";
|
|
20
|
+
export interface UserTurn {
|
|
21
|
+
/** Session log entry id of the user message that starts the turn. */
|
|
22
|
+
id: string;
|
|
23
|
+
/** Single-line preview of the user message. */
|
|
24
|
+
preview: string;
|
|
25
|
+
/** Full text of the user message. */
|
|
26
|
+
text: string;
|
|
27
|
+
timestamp: number;
|
|
28
|
+
}
|
|
29
|
+
export interface RewindResult {
|
|
30
|
+
/** Number of log entries removed. */
|
|
31
|
+
removedEntries: number;
|
|
32
|
+
/** Full text of the user message the session was rewound to (for re-editing). */
|
|
33
|
+
targetText: string;
|
|
34
|
+
}
|
|
19
35
|
export declare class SessionManager {
|
|
20
36
|
private sessionFile;
|
|
21
37
|
private log;
|
|
38
|
+
private checkpoints?;
|
|
22
39
|
constructor(sessionFile: string);
|
|
23
40
|
static create(cwd: string, sessionName?: string): SessionManager;
|
|
24
41
|
static resume(cwd: string, sessionName?: string): SessionManager | undefined;
|
|
@@ -41,6 +58,20 @@ export declare class SessionManager {
|
|
|
41
58
|
getTodos(): Todo[];
|
|
42
59
|
compact(options?: CompactOptions): CompactResult;
|
|
43
60
|
getMessages(): Message[];
|
|
61
|
+
/**
|
|
62
|
+
* Pre-edit file snapshot store for this session, used by /rewind.
|
|
63
|
+
* Lives next to the session JSONL as `<session>.checkpoints/`.
|
|
64
|
+
*/
|
|
65
|
+
getCheckpoints(): CheckpointStore;
|
|
66
|
+
/** Entry id of the most recent user message, or "0" before the first one. */
|
|
67
|
+
lastUserEntryId(): string;
|
|
68
|
+
/** User messages after the latest /clear, oldest first — the valid rewind anchors. */
|
|
69
|
+
listUserTurns(): UserTurn[];
|
|
70
|
+
/**
|
|
71
|
+
* Truncate the session to just before the user message with the given
|
|
72
|
+
* entry id. Returns undefined when the id does not name a user message.
|
|
73
|
+
*/
|
|
74
|
+
rewindToEntry(entryId: string): RewindResult | undefined;
|
|
44
75
|
getEntries(): SessionLogEntry[];
|
|
45
76
|
getSessionFile(): string;
|
|
46
77
|
private maybeAutoCompact;
|