@bubblebrain-ai/bubble 0.0.15 → 0.0.17
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/README.md +24 -0
- package/dist/agent/discovery-barrier.d.ts +21 -0
- package/dist/agent/discovery-barrier.js +173 -0
- package/dist/agent/internal-reminder-sanitizer.d.ts +9 -0
- package/dist/agent/internal-reminder-sanitizer.js +198 -0
- package/dist/agent/task-classifier.js +23 -5
- package/dist/agent.js +215 -30
- package/dist/context/budget.js +15 -0
- package/dist/context/projector.js +4 -3
- package/dist/debug-trace.js +14 -0
- package/dist/feishu/serve.js +1 -0
- package/dist/main.js +2 -0
- package/dist/model-catalog.d.ts +3 -0
- package/dist/model-catalog.js +44 -0
- package/dist/model-config.d.ts +3 -0
- package/dist/model-config.js +3 -0
- package/dist/model-pricing.d.ts +3 -2
- package/dist/model-pricing.js +8 -0
- package/dist/network/chatgpt-transport.d.ts +16 -0
- package/dist/network/chatgpt-transport.js +240 -0
- package/dist/oauth/openai-codex.d.ts +7 -2
- package/dist/oauth/openai-codex.js +7 -4
- package/dist/orchestrator/default-hooks.js +13 -2
- package/dist/orchestrator/hooks.d.ts +2 -0
- package/dist/prompt/compose.js +1 -1
- package/dist/prompt/reminders.js +3 -3
- package/dist/prompt/runtime.js +1 -0
- package/dist/provider-anthropic.d.ts +77 -0
- package/dist/provider-anthropic.js +544 -0
- package/dist/provider-openai-codex.d.ts +3 -0
- package/dist/provider-openai-codex.js +11 -2
- package/dist/provider-registry.d.ts +2 -0
- package/dist/provider-registry.js +29 -3
- package/dist/provider-transform.d.ts +1 -1
- package/dist/provider-transform.js +23 -0
- package/dist/provider.d.ts +4 -1
- package/dist/provider.js +119 -40
- package/dist/reasoning-debug.js +4 -1
- package/dist/session-log.js +17 -2
- package/dist/slash-commands/commands.js +4 -2
- package/dist/stats/usage.d.ts +4 -0
- package/dist/stats/usage.js +48 -11
- package/dist/tools/glob.js +3 -0
- package/dist/tools/grep.js +7 -0
- package/dist/tui/run.js +22 -12
- package/dist/tui-ink/app.js +3 -0
- package/dist/tui-ink/message-list.js +6 -3
- package/dist/tui-opentui/app.js +3 -0
- package/dist/tui-opentui/message-list.js +6 -3
- package/dist/types.d.ts +14 -1
- package/package.json +2 -1
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import { getAvailableThinkingLevels, normalizeThinkingLevel } from "./provider-transform.js";
|
|
2
|
+
const ANTHROPIC_VERSION = "2023-06-01";
|
|
3
|
+
const DEFAULT_MAX_TOKENS = 8192;
|
|
4
|
+
export function createAnthropicMessagesProvider(options) {
|
|
5
|
+
async function* streamChat(messages, chatOptions) {
|
|
6
|
+
const body = buildAnthropicRequest(options, messages, {
|
|
7
|
+
model: chatOptions.model,
|
|
8
|
+
tools: chatOptions.tools,
|
|
9
|
+
temperature: chatOptions.temperature,
|
|
10
|
+
thinkingLevel: chatOptions.thinkingLevel,
|
|
11
|
+
stream: true,
|
|
12
|
+
});
|
|
13
|
+
const response = await fetchAnthropicResponseWithRetry(options, {
|
|
14
|
+
url: resolveAnthropicMessagesUrl(options.baseURL),
|
|
15
|
+
stream: true,
|
|
16
|
+
method: "POST",
|
|
17
|
+
body: JSON.stringify(body),
|
|
18
|
+
signal: chatOptions.abortSignal,
|
|
19
|
+
});
|
|
20
|
+
yield* translateAnthropicStream(readSseEvents(response));
|
|
21
|
+
yield { type: "done" };
|
|
22
|
+
}
|
|
23
|
+
async function complete(messages, chatOptions) {
|
|
24
|
+
const body = buildAnthropicRequest(options, messages, {
|
|
25
|
+
model: chatOptions?.model ?? "claude-sonnet-4-6",
|
|
26
|
+
temperature: chatOptions?.temperature,
|
|
27
|
+
thinkingLevel: chatOptions?.thinkingLevel,
|
|
28
|
+
stream: false,
|
|
29
|
+
});
|
|
30
|
+
const response = await fetchAnthropicResponseWithRetry(options, {
|
|
31
|
+
url: resolveAnthropicMessagesUrl(options.baseURL),
|
|
32
|
+
stream: false,
|
|
33
|
+
method: "POST",
|
|
34
|
+
body: JSON.stringify(body),
|
|
35
|
+
signal: chatOptions?.abortSignal,
|
|
36
|
+
});
|
|
37
|
+
const data = await response.json();
|
|
38
|
+
return extractAnthropicText(data.content).join("");
|
|
39
|
+
}
|
|
40
|
+
return { streamChat, complete };
|
|
41
|
+
}
|
|
42
|
+
export function buildAnthropicRequest(options, messages, chatOptions) {
|
|
43
|
+
const { system, messages: anthropicMessages } = toAnthropicMessages(messages, shouldEchoThinking(options.providerId));
|
|
44
|
+
const tools = chatOptions.tools?.map((tool) => ({
|
|
45
|
+
name: tool.name,
|
|
46
|
+
description: tool.description,
|
|
47
|
+
input_schema: tool.parameters,
|
|
48
|
+
}));
|
|
49
|
+
const body = {
|
|
50
|
+
model: chatOptions.model,
|
|
51
|
+
max_tokens: DEFAULT_MAX_TOKENS,
|
|
52
|
+
system: system || undefined,
|
|
53
|
+
messages: anthropicMessages,
|
|
54
|
+
tools: tools && tools.length > 0 ? tools : undefined,
|
|
55
|
+
tool_choice: tools && tools.length > 0 ? { type: "auto" } : undefined,
|
|
56
|
+
stream: chatOptions.stream || undefined,
|
|
57
|
+
};
|
|
58
|
+
if (typeof chatOptions.temperature === "number") {
|
|
59
|
+
body.temperature = chatOptions.temperature;
|
|
60
|
+
}
|
|
61
|
+
const effectiveThinkingLevel = normalizeThinkingLevel(chatOptions.thinkingLevel ?? options.thinkingLevel ?? "off", getAvailableThinkingLevels(options.providerId || "", chatOptions.model));
|
|
62
|
+
if (effectiveThinkingLevel !== "off") {
|
|
63
|
+
body.thinking = { type: "adaptive" };
|
|
64
|
+
}
|
|
65
|
+
return body;
|
|
66
|
+
}
|
|
67
|
+
export function toAnthropicMessages(messages, echoThinking = false) {
|
|
68
|
+
const system = [];
|
|
69
|
+
const out = [];
|
|
70
|
+
const thinkingReplayIndexes = getThinkingReplayIndexes(messages, echoThinking);
|
|
71
|
+
for (let index = 0; index < messages.length; index++) {
|
|
72
|
+
const message = messages[index];
|
|
73
|
+
if (message.role === "system") {
|
|
74
|
+
system.push(message.content);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (message.role === "tool") {
|
|
78
|
+
pushAnthropicMessage(out, {
|
|
79
|
+
role: "user",
|
|
80
|
+
content: [{
|
|
81
|
+
type: "tool_result",
|
|
82
|
+
tool_use_id: message.toolCallId,
|
|
83
|
+
content: message.content,
|
|
84
|
+
...(message.isError ? { is_error: true } : {}),
|
|
85
|
+
}],
|
|
86
|
+
});
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (message.role === "assistant") {
|
|
90
|
+
const content = buildAssistantAnthropicBlocks(message, thinkingReplayIndexes.has(index));
|
|
91
|
+
if (content.length > 0) {
|
|
92
|
+
pushAnthropicMessage(out, { role: "assistant", content });
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
pushAnthropicMessage(out, {
|
|
97
|
+
role: "user",
|
|
98
|
+
content: typeof message.content === "string"
|
|
99
|
+
? message.content
|
|
100
|
+
: contentPartsToAnthropicBlocks(message.content),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return { system: system.join("\n\n"), messages: out };
|
|
104
|
+
}
|
|
105
|
+
function buildAssistantAnthropicBlocks(message, includeThinking) {
|
|
106
|
+
const rawBlocks = message.providerMetadata?.anthropic?.contentBlocks;
|
|
107
|
+
if (rawBlocks && rawBlocks.length > 0) {
|
|
108
|
+
const blocks = rawBlocks
|
|
109
|
+
.filter(isReplayableAssistantContentBlock)
|
|
110
|
+
.filter((block) => includeThinking || !isThinkingContentBlock(block))
|
|
111
|
+
.map((block) => cloneAnthropicContentBlock(block));
|
|
112
|
+
if (blocks.length > 0) {
|
|
113
|
+
return blocks;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const content = [];
|
|
117
|
+
if (includeThinking && message.reasoning?.trim()) {
|
|
118
|
+
content.push({ type: "thinking", thinking: message.reasoning });
|
|
119
|
+
}
|
|
120
|
+
if (message.content.trim()) {
|
|
121
|
+
content.push({ type: "text", text: message.content });
|
|
122
|
+
}
|
|
123
|
+
for (const toolCall of message.toolCalls ?? []) {
|
|
124
|
+
content.push({
|
|
125
|
+
type: "tool_use",
|
|
126
|
+
id: toolCall.id,
|
|
127
|
+
name: toolCall.name,
|
|
128
|
+
input: parseToolInput(toolCall.arguments),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return content;
|
|
132
|
+
}
|
|
133
|
+
function getThinkingReplayIndexes(messages, echoThinking) {
|
|
134
|
+
const indexes = new Set();
|
|
135
|
+
if (!echoThinking)
|
|
136
|
+
return indexes;
|
|
137
|
+
let lastUserIndex = -1;
|
|
138
|
+
for (let index = 0; index < messages.length; index++) {
|
|
139
|
+
if (messages[index].role === "user") {
|
|
140
|
+
lastUserIndex = index;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (let index = Math.max(0, lastUserIndex + 1); index < messages.length; index++) {
|
|
144
|
+
const message = messages[index];
|
|
145
|
+
if (message.role === "assistant" && assistantHasToolUse(message)) {
|
|
146
|
+
indexes.add(index);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return indexes;
|
|
150
|
+
}
|
|
151
|
+
function assistantHasToolUse(message) {
|
|
152
|
+
if (message.toolCalls && message.toolCalls.length > 0)
|
|
153
|
+
return true;
|
|
154
|
+
return message.providerMetadata?.anthropic?.contentBlocks?.some((block) => block.type === "tool_use") ?? false;
|
|
155
|
+
}
|
|
156
|
+
function isThinkingContentBlock(block) {
|
|
157
|
+
return block.type === "thinking" || block.type === "redacted_thinking";
|
|
158
|
+
}
|
|
159
|
+
function isReplayableAssistantContentBlock(block) {
|
|
160
|
+
switch (block.type) {
|
|
161
|
+
case "text":
|
|
162
|
+
return typeof block.text === "string";
|
|
163
|
+
case "thinking":
|
|
164
|
+
return typeof block.thinking === "string";
|
|
165
|
+
case "redacted_thinking":
|
|
166
|
+
return typeof block.data === "string";
|
|
167
|
+
case "tool_use":
|
|
168
|
+
return typeof block.id === "string" && typeof block.name === "string" && isObjectRecord(block.input);
|
|
169
|
+
default:
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function cloneAnthropicContentBlock(block) {
|
|
174
|
+
return JSON.parse(JSON.stringify(block));
|
|
175
|
+
}
|
|
176
|
+
export async function* translateAnthropicStream(events) {
|
|
177
|
+
const blocks = new Map();
|
|
178
|
+
let usage;
|
|
179
|
+
for await (const event of events) {
|
|
180
|
+
const type = typeof event.type === "string" ? event.type : "";
|
|
181
|
+
if (type === "message_start") {
|
|
182
|
+
usage = mergeAnthropicUsage(usage, event.message?.usage);
|
|
183
|
+
if (usage)
|
|
184
|
+
yield { type: "usage", usage };
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (type === "message_delta") {
|
|
188
|
+
usage = mergeAnthropicUsage(usage, event.usage);
|
|
189
|
+
if (usage)
|
|
190
|
+
yield { type: "usage", usage };
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (type === "error") {
|
|
194
|
+
const err = event.error;
|
|
195
|
+
throw new Error(`Anthropic stream error: ${String(err?.message || err?.type || "unknown error")}`);
|
|
196
|
+
}
|
|
197
|
+
if (type === "content_block_start") {
|
|
198
|
+
const index = typeof event.index === "number" ? event.index : 0;
|
|
199
|
+
const block = event.content_block;
|
|
200
|
+
const blockType = typeof block?.type === "string" ? block.type : "";
|
|
201
|
+
const raw = cloneProviderBlock(block, blockType);
|
|
202
|
+
const state = {
|
|
203
|
+
type: blockType,
|
|
204
|
+
id: typeof block?.id === "string" ? block.id : undefined,
|
|
205
|
+
name: typeof block?.name === "string" ? block.name : undefined,
|
|
206
|
+
args: "",
|
|
207
|
+
started: false,
|
|
208
|
+
input: isObjectRecord(block?.input) ? block.input : undefined,
|
|
209
|
+
raw,
|
|
210
|
+
text: typeof block?.text === "string" ? block.text : "",
|
|
211
|
+
thinking: typeof block?.thinking === "string" ? block.thinking : "",
|
|
212
|
+
signature: typeof block?.signature === "string" ? block.signature : "",
|
|
213
|
+
};
|
|
214
|
+
blocks.set(index, state);
|
|
215
|
+
if (blockType === "text" && typeof block?.text === "string" && block.text) {
|
|
216
|
+
yield { type: "text", content: block.text };
|
|
217
|
+
}
|
|
218
|
+
if (blockType === "thinking" && typeof block?.thinking === "string" && block.thinking) {
|
|
219
|
+
yield { type: "reasoning_delta", content: block.thinking };
|
|
220
|
+
}
|
|
221
|
+
if (blockType === "tool_use" && state.id && state.name) {
|
|
222
|
+
state.started = true;
|
|
223
|
+
yield { type: "tool_call", id: state.id, name: state.name, arguments: "", isStart: true, isEnd: false };
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (type === "content_block_delta") {
|
|
228
|
+
const index = typeof event.index === "number" ? event.index : 0;
|
|
229
|
+
const state = blocks.get(index);
|
|
230
|
+
const delta = event.delta;
|
|
231
|
+
const deltaType = typeof delta?.type === "string" ? delta.type : "";
|
|
232
|
+
if (deltaType === "text_delta" && typeof delta?.text === "string" && delta.text) {
|
|
233
|
+
if (state) {
|
|
234
|
+
state.text += delta.text;
|
|
235
|
+
state.raw.text = state.text;
|
|
236
|
+
}
|
|
237
|
+
yield { type: "text", content: delta.text };
|
|
238
|
+
}
|
|
239
|
+
else if (deltaType === "thinking_delta" && typeof delta?.thinking === "string" && delta.thinking) {
|
|
240
|
+
if (state) {
|
|
241
|
+
state.thinking += delta.thinking;
|
|
242
|
+
state.raw.thinking = state.thinking;
|
|
243
|
+
}
|
|
244
|
+
yield { type: "reasoning_delta", content: delta.thinking };
|
|
245
|
+
}
|
|
246
|
+
else if (deltaType === "signature_delta" && typeof delta?.signature === "string" && state) {
|
|
247
|
+
state.signature += delta.signature;
|
|
248
|
+
state.raw.signature = state.signature;
|
|
249
|
+
}
|
|
250
|
+
else if (deltaType === "input_json_delta" && state?.id && state.name && typeof delta?.partial_json === "string") {
|
|
251
|
+
state.args += delta.partial_json;
|
|
252
|
+
if (!state.started) {
|
|
253
|
+
state.started = true;
|
|
254
|
+
yield { type: "tool_call", id: state.id, name: state.name, arguments: "", isStart: true, isEnd: false };
|
|
255
|
+
}
|
|
256
|
+
if (delta.partial_json) {
|
|
257
|
+
yield { type: "tool_call", id: state.id, name: state.name, arguments: delta.partial_json, isStart: false, isEnd: false };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (type === "content_block_stop") {
|
|
263
|
+
const index = typeof event.index === "number" ? event.index : 0;
|
|
264
|
+
const state = blocks.get(index);
|
|
265
|
+
blocks.delete(index);
|
|
266
|
+
if (state?.type === "tool_use" && state.id && state.name) {
|
|
267
|
+
const finalArgs = state.args || JSON.stringify(state.input ?? {});
|
|
268
|
+
state.raw.input = parseToolInput(normalizeToolArgs(finalArgs));
|
|
269
|
+
yield { type: "provider_content_block", provider: "anthropic", block: state.raw };
|
|
270
|
+
yield {
|
|
271
|
+
type: "tool_call",
|
|
272
|
+
id: state.id,
|
|
273
|
+
name: state.name,
|
|
274
|
+
arguments: "",
|
|
275
|
+
argumentsFull: normalizeToolArgs(finalArgs),
|
|
276
|
+
isStart: false,
|
|
277
|
+
isEnd: true,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
else if (state && isReplayableAssistantContentBlock(state.raw)) {
|
|
281
|
+
finalizeRawContentBlock(state);
|
|
282
|
+
yield { type: "provider_content_block", provider: "anthropic", block: state.raw };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
export async function* readSseEvents(response) {
|
|
288
|
+
if (!response.body) {
|
|
289
|
+
throw new Error("Anthropic Messages API returned an empty stream body.");
|
|
290
|
+
}
|
|
291
|
+
const reader = response.body.getReader();
|
|
292
|
+
const decoder = new TextDecoder();
|
|
293
|
+
let buffer = "";
|
|
294
|
+
try {
|
|
295
|
+
while (true) {
|
|
296
|
+
const { done, value } = await reader.read();
|
|
297
|
+
if (done)
|
|
298
|
+
break;
|
|
299
|
+
buffer += decoder.decode(value, { stream: true });
|
|
300
|
+
let separator = buffer.indexOf("\n\n");
|
|
301
|
+
while (separator >= 0) {
|
|
302
|
+
const raw = buffer.slice(0, separator);
|
|
303
|
+
buffer = buffer.slice(separator + 2);
|
|
304
|
+
const event = parseSseEvent(raw);
|
|
305
|
+
if (event)
|
|
306
|
+
yield event;
|
|
307
|
+
separator = buffer.indexOf("\n\n");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
buffer += decoder.decode();
|
|
311
|
+
const event = parseSseEvent(buffer);
|
|
312
|
+
if (event)
|
|
313
|
+
yield event;
|
|
314
|
+
}
|
|
315
|
+
finally {
|
|
316
|
+
reader.releaseLock();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async function fetchAnthropicResponseWithRetry(options, request) {
|
|
320
|
+
const maxAttempts = shouldRetryMiniMaxAnthropic(options) ? 2 : 1;
|
|
321
|
+
let lastError;
|
|
322
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
323
|
+
const response = await fetch(request.url, {
|
|
324
|
+
method: request.method,
|
|
325
|
+
headers: buildAnthropicHeaders(options, request.stream),
|
|
326
|
+
body: request.body,
|
|
327
|
+
signal: request.signal,
|
|
328
|
+
});
|
|
329
|
+
if (response.ok)
|
|
330
|
+
return response;
|
|
331
|
+
const detail = await readAnthropicErrorDetail(response);
|
|
332
|
+
const error = new Error(`Anthropic Messages API error ${response.status}: ${detail || response.statusText}`);
|
|
333
|
+
lastError = error;
|
|
334
|
+
if (attempt >= maxAttempts || !isRetryableMiniMaxAnthropicError(response.status, detail)) {
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
await sleepBeforeRetry(getAnthropicRetryDelayMs(), request.signal);
|
|
338
|
+
}
|
|
339
|
+
throw lastError ?? new Error("Anthropic Messages API request failed");
|
|
340
|
+
}
|
|
341
|
+
function resolveAnthropicMessagesUrl(baseURL) {
|
|
342
|
+
const normalized = baseURL.trim().replace(/\/+$/, "");
|
|
343
|
+
if (normalized.endsWith("/v1/messages"))
|
|
344
|
+
return normalized;
|
|
345
|
+
if (normalized.endsWith("/v1"))
|
|
346
|
+
return `${normalized}/messages`;
|
|
347
|
+
return `${normalized || "https://api.anthropic.com"}/v1/messages`;
|
|
348
|
+
}
|
|
349
|
+
function buildAnthropicHeaders(options, stream) {
|
|
350
|
+
const headers = {
|
|
351
|
+
"content-type": "application/json",
|
|
352
|
+
"x-api-key": options.apiKey,
|
|
353
|
+
"anthropic-version": ANTHROPIC_VERSION,
|
|
354
|
+
};
|
|
355
|
+
if (shouldSendBearerAuth(options)) {
|
|
356
|
+
headers.authorization = `Bearer ${options.apiKey}`;
|
|
357
|
+
}
|
|
358
|
+
if (stream)
|
|
359
|
+
headers.accept = "text/event-stream";
|
|
360
|
+
return headers;
|
|
361
|
+
}
|
|
362
|
+
async function readAnthropicErrorDetail(response) {
|
|
363
|
+
try {
|
|
364
|
+
return await response.text();
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return response.statusText;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function shouldRetryMiniMaxAnthropic(options) {
|
|
371
|
+
const providerId = (options.providerId || "").toLowerCase();
|
|
372
|
+
const baseURL = options.baseURL.toLowerCase();
|
|
373
|
+
return providerId.startsWith("minimax") || baseURL.includes("api.minimaxi.com") || baseURL.includes("api.minimax.io");
|
|
374
|
+
}
|
|
375
|
+
function isRetryableMiniMaxAnthropicError(status, detail) {
|
|
376
|
+
return status === 500
|
|
377
|
+
|| status === 502
|
|
378
|
+
|| status === 503
|
|
379
|
+
|| status === 504
|
|
380
|
+
|| detail.includes("714 (1000)");
|
|
381
|
+
}
|
|
382
|
+
function getAnthropicRetryDelayMs() {
|
|
383
|
+
if (process.env.NODE_ENV === "test")
|
|
384
|
+
return 0;
|
|
385
|
+
return 800 + Math.floor(Math.random() * 700);
|
|
386
|
+
}
|
|
387
|
+
function sleepBeforeRetry(ms, signal) {
|
|
388
|
+
if (signal?.aborted) {
|
|
389
|
+
return Promise.reject(toAbortError(signal));
|
|
390
|
+
}
|
|
391
|
+
return new Promise((resolve, reject) => {
|
|
392
|
+
const timeout = setTimeout(resolve, ms);
|
|
393
|
+
signal?.addEventListener("abort", () => {
|
|
394
|
+
clearTimeout(timeout);
|
|
395
|
+
reject(toAbortError(signal));
|
|
396
|
+
}, { once: true });
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function toAbortError(signal) {
|
|
400
|
+
const reason = signal?.reason;
|
|
401
|
+
if (reason instanceof Error)
|
|
402
|
+
return reason;
|
|
403
|
+
const error = new Error("Anthropic request retry aborted.");
|
|
404
|
+
error.name = "AbortError";
|
|
405
|
+
return error;
|
|
406
|
+
}
|
|
407
|
+
function parseSseEvent(raw) {
|
|
408
|
+
const dataLines = [];
|
|
409
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
410
|
+
if (!line || line.startsWith(":"))
|
|
411
|
+
continue;
|
|
412
|
+
if (line.startsWith("data:")) {
|
|
413
|
+
dataLines.push(line.slice(5).trimStart());
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (dataLines.length === 0)
|
|
417
|
+
return undefined;
|
|
418
|
+
const data = dataLines.join("\n");
|
|
419
|
+
if (!data || data === "[DONE]")
|
|
420
|
+
return undefined;
|
|
421
|
+
return JSON.parse(data);
|
|
422
|
+
}
|
|
423
|
+
function cloneProviderBlock(block, fallbackType) {
|
|
424
|
+
const type = typeof block?.type === "string" && block.type ? block.type : fallbackType || "unknown";
|
|
425
|
+
const clone = block ? JSON.parse(JSON.stringify(block)) : {};
|
|
426
|
+
clone.type = type;
|
|
427
|
+
return clone;
|
|
428
|
+
}
|
|
429
|
+
function finalizeRawContentBlock(state) {
|
|
430
|
+
if (state.type === "text") {
|
|
431
|
+
state.raw.text = state.text;
|
|
432
|
+
}
|
|
433
|
+
else if (state.type === "thinking") {
|
|
434
|
+
state.raw.thinking = state.thinking;
|
|
435
|
+
if (state.signature) {
|
|
436
|
+
state.raw.signature = state.signature;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function contentPartsToAnthropicBlocks(parts) {
|
|
441
|
+
const blocks = [];
|
|
442
|
+
for (const part of parts) {
|
|
443
|
+
if (part.type === "text") {
|
|
444
|
+
blocks.push({ type: "text", text: part.text });
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
const image = part.image_url.url;
|
|
448
|
+
const dataUrlMatch = image.match(/^data:([^;,]+);base64,(.+)$/);
|
|
449
|
+
if (dataUrlMatch) {
|
|
450
|
+
blocks.push({
|
|
451
|
+
type: "image",
|
|
452
|
+
source: {
|
|
453
|
+
type: "base64",
|
|
454
|
+
media_type: dataUrlMatch[1],
|
|
455
|
+
data: dataUrlMatch[2],
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
blocks.push({ type: "image", source: { type: "url", url: image } });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return blocks;
|
|
464
|
+
}
|
|
465
|
+
function pushAnthropicMessage(messages, next) {
|
|
466
|
+
const last = messages.at(-1);
|
|
467
|
+
if (!last || last.role !== next.role) {
|
|
468
|
+
messages.push(next);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
last.content = mergeAnthropicContent(last.content, next.content);
|
|
472
|
+
}
|
|
473
|
+
function mergeAnthropicContent(current, next) {
|
|
474
|
+
const currentBlocks = typeof current === "string" ? [{ type: "text", text: current }] : current;
|
|
475
|
+
const nextBlocks = typeof next === "string" ? [{ type: "text", text: next }] : next;
|
|
476
|
+
return [...currentBlocks, ...nextBlocks];
|
|
477
|
+
}
|
|
478
|
+
function parseToolInput(raw) {
|
|
479
|
+
try {
|
|
480
|
+
const parsed = JSON.parse(raw || "{}");
|
|
481
|
+
return isObjectRecord(parsed) ? parsed : {};
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
return {};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function normalizeToolArgs(raw) {
|
|
488
|
+
try {
|
|
489
|
+
JSON.parse(raw);
|
|
490
|
+
return raw;
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
return "{}";
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function isObjectRecord(value) {
|
|
497
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
498
|
+
}
|
|
499
|
+
function extractAnthropicText(content) {
|
|
500
|
+
if (!content)
|
|
501
|
+
return [];
|
|
502
|
+
return content.flatMap((block) => block.type === "text" && typeof block.text === "string" ? [block.text] : []);
|
|
503
|
+
}
|
|
504
|
+
function mergeAnthropicUsage(current, raw) {
|
|
505
|
+
if (!isObjectRecord(raw))
|
|
506
|
+
return current;
|
|
507
|
+
const rawInput = typeof raw.input_tokens === "number" ? raw.input_tokens : undefined;
|
|
508
|
+
const rawCacheRead = typeof raw.cache_read_input_tokens === "number" ? raw.cache_read_input_tokens : undefined;
|
|
509
|
+
const rawCacheCreation = typeof raw.cache_creation_input_tokens === "number" ? raw.cache_creation_input_tokens : undefined;
|
|
510
|
+
const outputTokens = typeof raw.output_tokens === "number" ? raw.output_tokens : current?.completionTokens ?? 0;
|
|
511
|
+
const hasPromptUsage = rawInput !== undefined || rawCacheRead !== undefined || rawCacheCreation !== undefined;
|
|
512
|
+
let promptTokens = current?.promptTokens ?? 0;
|
|
513
|
+
let promptCacheHitTokens = current?.promptCacheHitTokens;
|
|
514
|
+
let promptCacheMissTokens = current?.promptCacheMissTokens;
|
|
515
|
+
if (hasPromptUsage) {
|
|
516
|
+
const inputTokens = rawInput ?? promptCacheMissTokens ?? promptTokens;
|
|
517
|
+
const cacheRead = rawCacheRead ?? promptCacheHitTokens ?? 0;
|
|
518
|
+
const cacheCreation = rawCacheCreation ?? 0;
|
|
519
|
+
promptTokens = inputTokens + cacheRead + cacheCreation;
|
|
520
|
+
promptCacheHitTokens = cacheRead;
|
|
521
|
+
promptCacheMissTokens = inputTokens + cacheCreation;
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
promptTokens,
|
|
525
|
+
completionTokens: outputTokens,
|
|
526
|
+
promptCacheHitTokens,
|
|
527
|
+
promptCacheMissTokens,
|
|
528
|
+
totalTokens: promptTokens + outputTokens,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
function shouldEchoThinking(providerId) {
|
|
532
|
+
return providerId?.startsWith("minimax") ?? false;
|
|
533
|
+
}
|
|
534
|
+
function shouldSendBearerAuth(options) {
|
|
535
|
+
return !isOfficialAnthropicBaseUrl(options.baseURL) || options.providerId?.startsWith("minimax") === true;
|
|
536
|
+
}
|
|
537
|
+
function isOfficialAnthropicBaseUrl(baseURL) {
|
|
538
|
+
try {
|
|
539
|
+
return new URL(baseURL).hostname === "api.anthropic.com";
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Provider, ReasoningEffort, ThinkingLevel, TokenUsage } from "./types.js";
|
|
2
2
|
import type { OAuthCredentials } from "./oauth/types.js";
|
|
3
|
+
import { type ChatGptFetch } from "./network/chatgpt-transport.js";
|
|
3
4
|
export interface CodexModelDescriptor {
|
|
4
5
|
id: string;
|
|
5
6
|
displayName?: string;
|
|
@@ -25,6 +26,7 @@ export declare function createOpenAICodexProvider(options: {
|
|
|
25
26
|
thinkingLevel?: ThinkingLevel;
|
|
26
27
|
promptCacheKey?: string;
|
|
27
28
|
auth?: OpenAICodexAuthAdapter;
|
|
29
|
+
fetch?: ChatGptFetch;
|
|
28
30
|
}): Provider;
|
|
29
31
|
export declare function normalizeOpenAICodexUsage(usage: any): TokenUsage;
|
|
30
32
|
export declare function buildOpenAICodexPromptCacheKey(input: {
|
|
@@ -35,5 +37,6 @@ export declare function buildOpenAICodexPromptCacheKey(input: {
|
|
|
35
37
|
export declare function fetchOpenAICodexModels(options: {
|
|
36
38
|
baseURL: string;
|
|
37
39
|
accessToken: string;
|
|
40
|
+
fetch?: ChatGptFetch;
|
|
38
41
|
}): Promise<CodexModelDescriptor[]>;
|
|
39
42
|
export declare function sortCodexModelDescriptors(descriptors: CodexModelDescriptor[]): CodexModelDescriptor[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { listBuiltinModels } from "./model-catalog.js";
|
|
3
3
|
import { resolveProviderRequestConfig } from "./provider-transform.js";
|
|
4
|
+
import { chatGptFetch } from "./network/chatgpt-transport.js";
|
|
4
5
|
const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
5
6
|
const OPENAI_BETA_RESPONSES = "responses=experimental";
|
|
6
7
|
const TOKEN_REFRESH_GRACE_MS = 5 * 60 * 1000;
|
|
@@ -42,6 +43,7 @@ export function extractChatGptAccountId(accessToken) {
|
|
|
42
43
|
}
|
|
43
44
|
export function createOpenAICodexProvider(options) {
|
|
44
45
|
const sessionId = globalThis.crypto?.randomUUID?.() ?? `bubble_${Date.now()}`;
|
|
46
|
+
const fetchImpl = options.fetch ?? chatGptFetch;
|
|
45
47
|
let refreshPromise;
|
|
46
48
|
async function resolveRequestAuth(forceRefresh = false) {
|
|
47
49
|
let credentials = await options.auth?.getCredentials();
|
|
@@ -77,7 +79,7 @@ export function createOpenAICodexProvider(options) {
|
|
|
77
79
|
}));
|
|
78
80
|
const sendRequest = async (forceRefresh = false) => {
|
|
79
81
|
const { accessToken, accountId } = await resolveRequestAuth(forceRefresh);
|
|
80
|
-
return
|
|
82
|
+
return fetchImpl(resolveCodexUrl(options.baseURL), buildCodexRequestInit({
|
|
81
83
|
accessToken,
|
|
82
84
|
accountId,
|
|
83
85
|
sessionId,
|
|
@@ -278,8 +280,9 @@ export async function fetchOpenAICodexModels(options) {
|
|
|
278
280
|
if (!accountId) {
|
|
279
281
|
return [];
|
|
280
282
|
}
|
|
283
|
+
const fetchImpl = options.fetch ?? chatGptFetch;
|
|
281
284
|
for (const path of MODEL_DISCOVERY_PATHS) {
|
|
282
|
-
const response = await
|
|
285
|
+
const response = await fetchImpl(resolveRelativeUrl(options.baseURL, path), {
|
|
283
286
|
method: "GET",
|
|
284
287
|
headers: buildBaseHeaders(options.accessToken, accountId, globalThis.crypto?.randomUUID?.() ?? `bubble_${Date.now()}`, { accept: "application/json" }),
|
|
285
288
|
}).catch(() => undefined);
|
|
@@ -451,6 +454,12 @@ function isTransientCodexTransportError(error) {
|
|
|
451
454
|
/\bEPIPE\b/i,
|
|
452
455
|
/socket hang up/i,
|
|
453
456
|
/fetch failed/i,
|
|
457
|
+
/unknown certificate verification error/i,
|
|
458
|
+
/certificate (?:verify|verification) (?:failed|error)/i,
|
|
459
|
+
/unable to verify (?:the )?(?:first )?certificate/i,
|
|
460
|
+
/UNABLE_TO_(?:VERIFY_LEAF_SIGNATURE|GET_ISSUER_CERT_LOCALLY)/i,
|
|
461
|
+
/SELF_SIGNED_CERT_IN_CHAIN/i,
|
|
462
|
+
/CERT_(?:HAS_EXPIRED|UNTRUSTED|INVALID)/i,
|
|
454
463
|
].some((pattern) => pattern.test(text));
|
|
455
464
|
}
|
|
456
465
|
function errorMessageChain(error) {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Reads provider configuration from models.json first, then falls back to config.json.
|
|
6
6
|
*/
|
|
7
7
|
import type { UserConfig } from "./config.js";
|
|
8
|
+
import { type ProviderProtocol } from "./model-catalog.js";
|
|
8
9
|
import { ModelConfig } from "./model-config.js";
|
|
9
10
|
import { AuthStorage } from "./oauth/index.js";
|
|
10
11
|
import { type OpenAICodexAuthAdapter } from "./provider-openai-codex.js";
|
|
@@ -15,6 +16,7 @@ export interface ProviderProfile {
|
|
|
15
16
|
apiKey: string;
|
|
16
17
|
enabled: boolean;
|
|
17
18
|
authType?: "api" | "oauth";
|
|
19
|
+
protocol?: ProviderProtocol;
|
|
18
20
|
}
|
|
19
21
|
export interface ModelInfo {
|
|
20
22
|
id: string;
|
|
@@ -11,7 +11,7 @@ import { fetchOpenAICodexModels } from "./provider-openai-codex.js";
|
|
|
11
11
|
import { refreshOpenAICodex } from "./oauth/openai-codex.js";
|
|
12
12
|
export const BUILTIN_PROVIDERS = CATALOG_PROVIDERS;
|
|
13
13
|
export const USER_VISIBLE_PROVIDER_IDS = BUILTIN_PROVIDERS
|
|
14
|
-
.filter((provider) => provider.id !== "openrouter" && provider.id !== "openai-codex")
|
|
14
|
+
.filter((provider) => !provider.hidden && provider.id !== "openrouter" && provider.id !== "openai-codex")
|
|
15
15
|
.map((provider) => provider.id);
|
|
16
16
|
export function isUserVisibleProvider(providerId) {
|
|
17
17
|
return USER_VISIBLE_PROVIDER_IDS.includes(providerId);
|
|
@@ -123,19 +123,27 @@ export class ProviderRegistry {
|
|
|
123
123
|
providers = keys.map((id) => {
|
|
124
124
|
const builtin = getBuiltinProvider(id);
|
|
125
125
|
const cfg = modelsJsonProviders[id];
|
|
126
|
+
const baseURL = cfg.baseURL || builtin?.baseURL || "";
|
|
126
127
|
return {
|
|
127
128
|
id,
|
|
128
129
|
name: builtin?.name || id,
|
|
129
|
-
baseURL
|
|
130
|
+
baseURL,
|
|
130
131
|
apiKey: cfg.apiKey || "",
|
|
131
132
|
enabled: true,
|
|
132
133
|
authType: "api",
|
|
134
|
+
protocol: resolveConfiguredProtocol(id, baseURL, cfg.protocol),
|
|
133
135
|
};
|
|
134
136
|
});
|
|
135
137
|
}
|
|
136
138
|
else {
|
|
137
139
|
// 2. Fall back to config.json providers (interactive TUI style)
|
|
138
|
-
providers = this.config.getProviders()
|
|
140
|
+
providers = this.config.getProviders().map((provider) => {
|
|
141
|
+
const builtin = getBuiltinProvider(provider.id);
|
|
142
|
+
return {
|
|
143
|
+
...provider,
|
|
144
|
+
protocol: resolveConfiguredProtocol(provider.id, provider.baseURL, provider.protocol),
|
|
145
|
+
};
|
|
146
|
+
});
|
|
139
147
|
}
|
|
140
148
|
// 3. Inject OAuth access tokens
|
|
141
149
|
for (const p of providers) {
|
|
@@ -283,6 +291,24 @@ export class ProviderRegistry {
|
|
|
283
291
|
}));
|
|
284
292
|
}
|
|
285
293
|
}
|
|
294
|
+
function resolveConfiguredProtocol(providerId, baseURL, explicitProtocol) {
|
|
295
|
+
if (explicitProtocol)
|
|
296
|
+
return explicitProtocol;
|
|
297
|
+
const builtin = getBuiltinProvider(providerId);
|
|
298
|
+
if (!builtin?.protocol)
|
|
299
|
+
return undefined;
|
|
300
|
+
const normalizedBaseURL = normalizeBaseURL(baseURL);
|
|
301
|
+
if (!normalizedBaseURL || normalizedBaseURL === normalizeBaseURL(builtin.baseURL)) {
|
|
302
|
+
return builtin.protocol;
|
|
303
|
+
}
|
|
304
|
+
if (normalizedBaseURL.includes("/anthropic")) {
|
|
305
|
+
return "anthropic-messages";
|
|
306
|
+
}
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
function normalizeBaseURL(baseURL) {
|
|
310
|
+
return baseURL.trim().replace(/\/+$/, "").toLowerCase();
|
|
311
|
+
}
|
|
286
312
|
/** Encode a model selection as "providerId:modelId". */
|
|
287
313
|
export function encodeModel(providerId, modelId) {
|
|
288
314
|
return `${providerId}:${modelId}`;
|