@bubblebrain-ai/bubble 0.0.16 → 0.0.18
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 +2 -0
- package/dist/agent/internal-reminder-sanitizer.js +27 -0
- package/dist/agent/tool-intent.js +0 -1
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +148 -23
- package/dist/context/budget.js +15 -0
- package/dist/context/prune.d.ts +1 -0
- package/dist/context/prune.js +32 -0
- package/dist/debug-trace.js +14 -0
- package/dist/feishu/agent-host/run-driver.js +2 -2
- package/dist/feishu/card/run-state.js +1 -0
- package/dist/feishu/serve.js +1 -0
- package/dist/main.js +13 -9
- package/dist/model-catalog.d.ts +3 -0
- package/dist/model-catalog.js +38 -0
- package/dist/model-config.d.ts +3 -0
- package/dist/model-config.js +3 -0
- package/dist/model-pricing.js +2 -1
- package/dist/model-selection.d.ts +7 -0
- package/dist/model-selection.js +9 -0
- package/dist/network/chatgpt-transport.js +1 -0
- package/dist/orchestrator/default-hooks.js +1 -1
- package/dist/prompt/compose.js +1 -1
- package/dist/prompt/environment.js +1 -3
- package/dist/prompt/reminders.js +3 -3
- package/dist/prompt/runtime.js +2 -1
- package/dist/provider-anthropic.d.ts +89 -0
- package/dist/provider-anthropic.js +597 -0
- package/dist/provider-openai-codex.js +3 -1
- 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 +14 -0
- package/dist/provider.d.ts +4 -1
- package/dist/provider.js +120 -41
- package/dist/session-log.js +14 -2
- package/dist/session-title.js +3 -6
- package/dist/slash-commands/commands.js +8 -2
- package/dist/stats/usage.d.ts +1 -0
- package/dist/stats/usage.js +28 -3
- package/dist/tools/edit.js +75 -1
- package/dist/tools/glob.js +77 -12
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +1 -3
- package/dist/tools/prompt-metadata.d.ts +3 -0
- package/dist/tools/prompt-metadata.js +17 -0
- package/dist/tools/write.js +14 -0
- package/dist/tui/paste-placeholder.d.ts +10 -0
- package/dist/tui/paste-placeholder.js +45 -0
- package/dist/tui/run.js +23 -0
- package/dist/tui-ink/app.js +2 -0
- package/dist/tui-ink/input-box.d.ts +1 -8
- package/dist/tui-ink/input-box.js +8 -38
- package/dist/tui-opentui/app.js +2 -0
- package/dist/tui-opentui/input-box.d.ts +1 -3
- package/dist/tui-opentui/input-box.js +17 -26
- package/dist/types.d.ts +22 -0
- package/package.json +7 -3
- package/dist/tools/apply-patch.d.ts +0 -9
- package/dist/tools/apply-patch.js +0 -330
- package/dist/tools/patch-apply.d.ts +0 -41
- package/dist/tools/patch-apply.js +0 -312
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import { getAvailableThinkingLevels, normalizeThinkingLevel } from "./provider-transform.js";
|
|
2
|
+
const ANTHROPIC_VERSION = "2023-06-01";
|
|
3
|
+
const DEFAULT_MAX_TOKENS = 8192;
|
|
4
|
+
const ANTHROPIC_PROMPT_CACHE_CONTROL = { type: "ephemeral" };
|
|
5
|
+
const MINIMAX_PROMPT_CACHE_MODELS = new Set([
|
|
6
|
+
"minimax-m2.7",
|
|
7
|
+
"minimax-m2.7-highspeed",
|
|
8
|
+
"minimax-m2.5",
|
|
9
|
+
"minimax-m2.5-highspeed",
|
|
10
|
+
"minimax-m2.1",
|
|
11
|
+
"minimax-m2.1-highspeed",
|
|
12
|
+
"minimax-m2",
|
|
13
|
+
"m2-her",
|
|
14
|
+
]);
|
|
15
|
+
export function createAnthropicMessagesProvider(options) {
|
|
16
|
+
async function* streamChat(messages, chatOptions) {
|
|
17
|
+
const body = buildAnthropicRequest(options, messages, {
|
|
18
|
+
model: chatOptions.model,
|
|
19
|
+
tools: chatOptions.tools,
|
|
20
|
+
toolChoice: chatOptions.toolChoice,
|
|
21
|
+
temperature: chatOptions.temperature,
|
|
22
|
+
thinkingLevel: chatOptions.thinkingLevel,
|
|
23
|
+
stream: true,
|
|
24
|
+
});
|
|
25
|
+
const response = await fetchAnthropicResponseWithRetry(options, {
|
|
26
|
+
url: resolveAnthropicMessagesUrl(options.baseURL),
|
|
27
|
+
stream: true,
|
|
28
|
+
method: "POST",
|
|
29
|
+
body: JSON.stringify(body),
|
|
30
|
+
signal: chatOptions.abortSignal,
|
|
31
|
+
});
|
|
32
|
+
yield* translateAnthropicStream(readSseEvents(response));
|
|
33
|
+
yield { type: "done" };
|
|
34
|
+
}
|
|
35
|
+
async function complete(messages, chatOptions) {
|
|
36
|
+
const body = buildAnthropicRequest(options, messages, {
|
|
37
|
+
model: chatOptions?.model ?? "claude-sonnet-4-6",
|
|
38
|
+
temperature: chatOptions?.temperature,
|
|
39
|
+
thinkingLevel: chatOptions?.thinkingLevel,
|
|
40
|
+
stream: false,
|
|
41
|
+
});
|
|
42
|
+
const response = await fetchAnthropicResponseWithRetry(options, {
|
|
43
|
+
url: resolveAnthropicMessagesUrl(options.baseURL),
|
|
44
|
+
stream: false,
|
|
45
|
+
method: "POST",
|
|
46
|
+
body: JSON.stringify(body),
|
|
47
|
+
signal: chatOptions?.abortSignal,
|
|
48
|
+
});
|
|
49
|
+
const data = await response.json();
|
|
50
|
+
return extractAnthropicText(data.content).join("");
|
|
51
|
+
}
|
|
52
|
+
return { streamChat, complete };
|
|
53
|
+
}
|
|
54
|
+
export function buildAnthropicRequest(options, messages, chatOptions) {
|
|
55
|
+
const { system, messages: anthropicMessages } = toAnthropicMessages(messages, shouldEchoThinking(options.providerId));
|
|
56
|
+
const enablePromptCache = supportsAnthropicPromptCache(options, chatOptions.model);
|
|
57
|
+
const tools = chatOptions.tools?.map((tool) => ({
|
|
58
|
+
name: tool.name,
|
|
59
|
+
description: tool.description,
|
|
60
|
+
input_schema: tool.parameters,
|
|
61
|
+
}));
|
|
62
|
+
if (enablePromptCache && tools && tools.length > 0) {
|
|
63
|
+
tools[tools.length - 1] = {
|
|
64
|
+
...tools[tools.length - 1],
|
|
65
|
+
cache_control: ANTHROPIC_PROMPT_CACHE_CONTROL,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const body = {
|
|
69
|
+
model: chatOptions.model,
|
|
70
|
+
max_tokens: DEFAULT_MAX_TOKENS,
|
|
71
|
+
system: buildAnthropicSystem(system, enablePromptCache),
|
|
72
|
+
messages: anthropicMessages,
|
|
73
|
+
tools: tools && tools.length > 0 ? tools : undefined,
|
|
74
|
+
tool_choice: tools && tools.length > 0 ? { type: chatOptions.toolChoice ?? "auto" } : undefined,
|
|
75
|
+
stream: chatOptions.stream || undefined,
|
|
76
|
+
};
|
|
77
|
+
if (typeof chatOptions.temperature === "number") {
|
|
78
|
+
body.temperature = chatOptions.temperature;
|
|
79
|
+
}
|
|
80
|
+
const effectiveThinkingLevel = normalizeThinkingLevel(chatOptions.thinkingLevel ?? options.thinkingLevel ?? "off", getAvailableThinkingLevels(options.providerId || "", chatOptions.model));
|
|
81
|
+
if (effectiveThinkingLevel !== "off") {
|
|
82
|
+
body.thinking = { type: "adaptive" };
|
|
83
|
+
}
|
|
84
|
+
return body;
|
|
85
|
+
}
|
|
86
|
+
function buildAnthropicSystem(system, enablePromptCache) {
|
|
87
|
+
if (!system)
|
|
88
|
+
return undefined;
|
|
89
|
+
if (!enablePromptCache)
|
|
90
|
+
return system;
|
|
91
|
+
return [{ type: "text", text: system, cache_control: ANTHROPIC_PROMPT_CACHE_CONTROL }];
|
|
92
|
+
}
|
|
93
|
+
export function supportsAnthropicPromptCache(options, model) {
|
|
94
|
+
const providerId = (options.providerId ?? "").toLowerCase();
|
|
95
|
+
if (providerId === "anthropic" || isOfficialAnthropicBaseUrl(options.baseURL)) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
if (!isMiniMaxAnthropicEndpoint(options)) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return MINIMAX_PROMPT_CACHE_MODELS.has(model.toLowerCase());
|
|
102
|
+
}
|
|
103
|
+
export function toAnthropicMessages(messages, echoThinking = false) {
|
|
104
|
+
const system = [];
|
|
105
|
+
const out = [];
|
|
106
|
+
const thinkingReplayIndexes = getThinkingReplayIndexes(messages, echoThinking);
|
|
107
|
+
for (let index = 0; index < messages.length; index++) {
|
|
108
|
+
const message = messages[index];
|
|
109
|
+
if (message.role === "system") {
|
|
110
|
+
system.push(message.content);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (message.role === "tool") {
|
|
114
|
+
pushAnthropicMessage(out, {
|
|
115
|
+
role: "user",
|
|
116
|
+
content: [{
|
|
117
|
+
type: "tool_result",
|
|
118
|
+
tool_use_id: message.toolCallId,
|
|
119
|
+
content: message.content,
|
|
120
|
+
...(message.isError ? { is_error: true } : {}),
|
|
121
|
+
}],
|
|
122
|
+
});
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (message.role === "assistant") {
|
|
126
|
+
const content = buildAssistantAnthropicBlocks(message, thinkingReplayIndexes.has(index));
|
|
127
|
+
if (content.length > 0) {
|
|
128
|
+
pushAnthropicMessage(out, { role: "assistant", content });
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
pushAnthropicMessage(out, {
|
|
133
|
+
role: "user",
|
|
134
|
+
content: typeof message.content === "string"
|
|
135
|
+
? message.content
|
|
136
|
+
: contentPartsToAnthropicBlocks(message.content),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return { system: system.join("\n\n"), messages: out };
|
|
140
|
+
}
|
|
141
|
+
function buildAssistantAnthropicBlocks(message, includeThinking) {
|
|
142
|
+
const rawBlocks = message.providerMetadata?.anthropic?.contentBlocks;
|
|
143
|
+
if (rawBlocks && rawBlocks.length > 0) {
|
|
144
|
+
const blocks = rawBlocks
|
|
145
|
+
.filter(isReplayableAssistantContentBlock)
|
|
146
|
+
.filter((block) => includeThinking || !isThinkingContentBlock(block))
|
|
147
|
+
.map((block) => cloneAnthropicContentBlock(block));
|
|
148
|
+
if (blocks.length > 0) {
|
|
149
|
+
return blocks;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const content = [];
|
|
153
|
+
if (includeThinking && message.reasoning?.trim()) {
|
|
154
|
+
content.push({ type: "thinking", thinking: message.reasoning });
|
|
155
|
+
}
|
|
156
|
+
if (message.content.trim()) {
|
|
157
|
+
content.push({ type: "text", text: message.content });
|
|
158
|
+
}
|
|
159
|
+
for (const toolCall of message.toolCalls ?? []) {
|
|
160
|
+
content.push({
|
|
161
|
+
type: "tool_use",
|
|
162
|
+
id: toolCall.id,
|
|
163
|
+
name: toolCall.name,
|
|
164
|
+
input: parseToolInput(toolCall.arguments),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return content;
|
|
168
|
+
}
|
|
169
|
+
function getThinkingReplayIndexes(messages, echoThinking) {
|
|
170
|
+
const indexes = new Set();
|
|
171
|
+
if (!echoThinking)
|
|
172
|
+
return indexes;
|
|
173
|
+
let lastUserIndex = -1;
|
|
174
|
+
for (let index = 0; index < messages.length; index++) {
|
|
175
|
+
if (messages[index].role === "user") {
|
|
176
|
+
lastUserIndex = index;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (let index = Math.max(0, lastUserIndex + 1); index < messages.length; index++) {
|
|
180
|
+
const message = messages[index];
|
|
181
|
+
if (message.role === "assistant" && assistantHasToolUse(message)) {
|
|
182
|
+
indexes.add(index);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return indexes;
|
|
186
|
+
}
|
|
187
|
+
function assistantHasToolUse(message) {
|
|
188
|
+
if (message.toolCalls && message.toolCalls.length > 0)
|
|
189
|
+
return true;
|
|
190
|
+
return message.providerMetadata?.anthropic?.contentBlocks?.some((block) => block.type === "tool_use") ?? false;
|
|
191
|
+
}
|
|
192
|
+
function isThinkingContentBlock(block) {
|
|
193
|
+
return block.type === "thinking" || block.type === "redacted_thinking";
|
|
194
|
+
}
|
|
195
|
+
function isReplayableAssistantContentBlock(block) {
|
|
196
|
+
switch (block.type) {
|
|
197
|
+
case "text":
|
|
198
|
+
return typeof block.text === "string";
|
|
199
|
+
case "thinking":
|
|
200
|
+
return typeof block.thinking === "string";
|
|
201
|
+
case "redacted_thinking":
|
|
202
|
+
return typeof block.data === "string";
|
|
203
|
+
case "tool_use":
|
|
204
|
+
return typeof block.id === "string" && typeof block.name === "string" && isObjectRecord(block.input);
|
|
205
|
+
default:
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function cloneAnthropicContentBlock(block) {
|
|
210
|
+
return JSON.parse(JSON.stringify(block));
|
|
211
|
+
}
|
|
212
|
+
export async function* translateAnthropicStream(events) {
|
|
213
|
+
const blocks = new Map();
|
|
214
|
+
let usage;
|
|
215
|
+
for await (const event of events) {
|
|
216
|
+
const type = typeof event.type === "string" ? event.type : "";
|
|
217
|
+
if (type === "message_start") {
|
|
218
|
+
usage = mergeAnthropicUsage(usage, event.message?.usage);
|
|
219
|
+
if (usage)
|
|
220
|
+
yield { type: "usage", usage };
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (type === "message_delta") {
|
|
224
|
+
usage = mergeAnthropicUsage(usage, event.usage);
|
|
225
|
+
if (usage)
|
|
226
|
+
yield { type: "usage", usage };
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (type === "error") {
|
|
230
|
+
const err = event.error;
|
|
231
|
+
throw new Error(`Anthropic stream error: ${String(err?.message || err?.type || "unknown error")}`);
|
|
232
|
+
}
|
|
233
|
+
if (type === "content_block_start") {
|
|
234
|
+
const index = typeof event.index === "number" ? event.index : 0;
|
|
235
|
+
const block = event.content_block;
|
|
236
|
+
const blockType = typeof block?.type === "string" ? block.type : "";
|
|
237
|
+
const raw = cloneProviderBlock(block, blockType);
|
|
238
|
+
const state = {
|
|
239
|
+
type: blockType,
|
|
240
|
+
id: typeof block?.id === "string" ? block.id : undefined,
|
|
241
|
+
name: typeof block?.name === "string" ? block.name : undefined,
|
|
242
|
+
args: "",
|
|
243
|
+
started: false,
|
|
244
|
+
input: isObjectRecord(block?.input) ? block.input : undefined,
|
|
245
|
+
raw,
|
|
246
|
+
text: typeof block?.text === "string" ? block.text : "",
|
|
247
|
+
thinking: typeof block?.thinking === "string" ? block.thinking : "",
|
|
248
|
+
signature: typeof block?.signature === "string" ? block.signature : "",
|
|
249
|
+
};
|
|
250
|
+
blocks.set(index, state);
|
|
251
|
+
if (blockType === "text" && typeof block?.text === "string" && block.text) {
|
|
252
|
+
yield { type: "text", content: block.text };
|
|
253
|
+
}
|
|
254
|
+
if (blockType === "thinking" && typeof block?.thinking === "string" && block.thinking) {
|
|
255
|
+
yield { type: "reasoning_delta", content: block.thinking };
|
|
256
|
+
}
|
|
257
|
+
if (blockType === "tool_use" && state.id && state.name) {
|
|
258
|
+
state.started = true;
|
|
259
|
+
yield { type: "tool_call", id: state.id, name: state.name, arguments: "", isStart: true, isEnd: false };
|
|
260
|
+
}
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (type === "content_block_delta") {
|
|
264
|
+
const index = typeof event.index === "number" ? event.index : 0;
|
|
265
|
+
const state = blocks.get(index);
|
|
266
|
+
const delta = event.delta;
|
|
267
|
+
const deltaType = typeof delta?.type === "string" ? delta.type : "";
|
|
268
|
+
if (deltaType === "text_delta" && typeof delta?.text === "string" && delta.text) {
|
|
269
|
+
if (state) {
|
|
270
|
+
state.text += delta.text;
|
|
271
|
+
state.raw.text = state.text;
|
|
272
|
+
}
|
|
273
|
+
yield { type: "text", content: delta.text };
|
|
274
|
+
}
|
|
275
|
+
else if (deltaType === "thinking_delta" && typeof delta?.thinking === "string" && delta.thinking) {
|
|
276
|
+
if (state) {
|
|
277
|
+
state.thinking += delta.thinking;
|
|
278
|
+
state.raw.thinking = state.thinking;
|
|
279
|
+
}
|
|
280
|
+
yield { type: "reasoning_delta", content: delta.thinking };
|
|
281
|
+
}
|
|
282
|
+
else if (deltaType === "signature_delta" && typeof delta?.signature === "string" && state) {
|
|
283
|
+
state.signature += delta.signature;
|
|
284
|
+
state.raw.signature = state.signature;
|
|
285
|
+
}
|
|
286
|
+
else if (deltaType === "input_json_delta" && state?.id && state.name && typeof delta?.partial_json === "string") {
|
|
287
|
+
state.args += delta.partial_json;
|
|
288
|
+
if (!state.started) {
|
|
289
|
+
state.started = true;
|
|
290
|
+
yield { type: "tool_call", id: state.id, name: state.name, arguments: "", isStart: true, isEnd: false };
|
|
291
|
+
}
|
|
292
|
+
if (delta.partial_json) {
|
|
293
|
+
yield { type: "tool_call", id: state.id, name: state.name, arguments: delta.partial_json, isStart: false, isEnd: false };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (type === "content_block_stop") {
|
|
299
|
+
const index = typeof event.index === "number" ? event.index : 0;
|
|
300
|
+
const state = blocks.get(index);
|
|
301
|
+
blocks.delete(index);
|
|
302
|
+
if (state?.type === "tool_use" && state.id && state.name) {
|
|
303
|
+
const finalArgs = state.args || JSON.stringify(state.input ?? {});
|
|
304
|
+
state.raw.input = parseToolInput(normalizeToolArgs(finalArgs));
|
|
305
|
+
yield { type: "provider_content_block", provider: "anthropic", block: state.raw };
|
|
306
|
+
yield {
|
|
307
|
+
type: "tool_call",
|
|
308
|
+
id: state.id,
|
|
309
|
+
name: state.name,
|
|
310
|
+
arguments: "",
|
|
311
|
+
argumentsFull: normalizeToolArgs(finalArgs),
|
|
312
|
+
isStart: false,
|
|
313
|
+
isEnd: true,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
else if (state && isReplayableAssistantContentBlock(state.raw)) {
|
|
317
|
+
finalizeRawContentBlock(state);
|
|
318
|
+
yield { type: "provider_content_block", provider: "anthropic", block: state.raw };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
export async function* readSseEvents(response) {
|
|
324
|
+
if (!response.body) {
|
|
325
|
+
throw new Error("Anthropic Messages API returned an empty stream body.");
|
|
326
|
+
}
|
|
327
|
+
const reader = response.body.getReader();
|
|
328
|
+
const decoder = new TextDecoder();
|
|
329
|
+
let buffer = "";
|
|
330
|
+
try {
|
|
331
|
+
while (true) {
|
|
332
|
+
const { done, value } = await reader.read();
|
|
333
|
+
if (done)
|
|
334
|
+
break;
|
|
335
|
+
buffer += decoder.decode(value, { stream: true });
|
|
336
|
+
let separator = buffer.indexOf("\n\n");
|
|
337
|
+
while (separator >= 0) {
|
|
338
|
+
const raw = buffer.slice(0, separator);
|
|
339
|
+
buffer = buffer.slice(separator + 2);
|
|
340
|
+
const event = parseSseEvent(raw);
|
|
341
|
+
if (event)
|
|
342
|
+
yield event;
|
|
343
|
+
separator = buffer.indexOf("\n\n");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
buffer += decoder.decode();
|
|
347
|
+
const event = parseSseEvent(buffer);
|
|
348
|
+
if (event)
|
|
349
|
+
yield event;
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
reader.releaseLock();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async function fetchAnthropicResponseWithRetry(options, request) {
|
|
356
|
+
const maxAttempts = shouldRetryMiniMaxAnthropic(options) ? 2 : 1;
|
|
357
|
+
let lastError;
|
|
358
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
359
|
+
const response = await fetch(request.url, {
|
|
360
|
+
method: request.method,
|
|
361
|
+
headers: buildAnthropicHeaders(options, request.stream),
|
|
362
|
+
body: request.body,
|
|
363
|
+
signal: request.signal,
|
|
364
|
+
});
|
|
365
|
+
if (response.ok)
|
|
366
|
+
return response;
|
|
367
|
+
const detail = await readAnthropicErrorDetail(response);
|
|
368
|
+
const error = new Error(`Anthropic Messages API error ${response.status}: ${detail || response.statusText}`);
|
|
369
|
+
lastError = error;
|
|
370
|
+
if (attempt >= maxAttempts || !isRetryableMiniMaxAnthropicError(response.status, detail)) {
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
await sleepBeforeRetry(getAnthropicRetryDelayMs(), request.signal);
|
|
374
|
+
}
|
|
375
|
+
throw lastError ?? new Error("Anthropic Messages API request failed");
|
|
376
|
+
}
|
|
377
|
+
function resolveAnthropicMessagesUrl(baseURL) {
|
|
378
|
+
const normalized = baseURL.trim().replace(/\/+$/, "");
|
|
379
|
+
if (normalized.endsWith("/v1/messages"))
|
|
380
|
+
return normalized;
|
|
381
|
+
if (normalized.endsWith("/v1"))
|
|
382
|
+
return `${normalized}/messages`;
|
|
383
|
+
return `${normalized || "https://api.anthropic.com"}/v1/messages`;
|
|
384
|
+
}
|
|
385
|
+
function buildAnthropicHeaders(options, stream) {
|
|
386
|
+
const headers = {
|
|
387
|
+
"content-type": "application/json",
|
|
388
|
+
"x-api-key": options.apiKey,
|
|
389
|
+
"anthropic-version": ANTHROPIC_VERSION,
|
|
390
|
+
};
|
|
391
|
+
if (shouldSendBearerAuth(options)) {
|
|
392
|
+
headers.authorization = `Bearer ${options.apiKey}`;
|
|
393
|
+
}
|
|
394
|
+
if (stream)
|
|
395
|
+
headers.accept = "text/event-stream";
|
|
396
|
+
return headers;
|
|
397
|
+
}
|
|
398
|
+
async function readAnthropicErrorDetail(response) {
|
|
399
|
+
try {
|
|
400
|
+
return await response.text();
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return response.statusText;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function shouldRetryMiniMaxAnthropic(options) {
|
|
407
|
+
const providerId = (options.providerId || "").toLowerCase();
|
|
408
|
+
const baseURL = options.baseURL.toLowerCase();
|
|
409
|
+
return providerId.startsWith("minimax") || baseURL.includes("api.minimaxi.com") || baseURL.includes("api.minimax.io");
|
|
410
|
+
}
|
|
411
|
+
function isRetryableMiniMaxAnthropicError(status, detail) {
|
|
412
|
+
return status === 500
|
|
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 });
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
function toAbortError(signal) {
|
|
436
|
+
const reason = signal?.reason;
|
|
437
|
+
if (reason instanceof Error)
|
|
438
|
+
return reason;
|
|
439
|
+
const error = new Error("Anthropic request retry aborted.");
|
|
440
|
+
error.name = "AbortError";
|
|
441
|
+
return error;
|
|
442
|
+
}
|
|
443
|
+
function parseSseEvent(raw) {
|
|
444
|
+
const dataLines = [];
|
|
445
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
446
|
+
if (!line || line.startsWith(":"))
|
|
447
|
+
continue;
|
|
448
|
+
if (line.startsWith("data:")) {
|
|
449
|
+
dataLines.push(line.slice(5).trimStart());
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (dataLines.length === 0)
|
|
453
|
+
return undefined;
|
|
454
|
+
const data = dataLines.join("\n");
|
|
455
|
+
if (!data || data === "[DONE]")
|
|
456
|
+
return undefined;
|
|
457
|
+
return JSON.parse(data);
|
|
458
|
+
}
|
|
459
|
+
function cloneProviderBlock(block, fallbackType) {
|
|
460
|
+
const type = typeof block?.type === "string" && block.type ? block.type : fallbackType || "unknown";
|
|
461
|
+
const clone = block ? JSON.parse(JSON.stringify(block)) : {};
|
|
462
|
+
clone.type = type;
|
|
463
|
+
return clone;
|
|
464
|
+
}
|
|
465
|
+
function finalizeRawContentBlock(state) {
|
|
466
|
+
if (state.type === "text") {
|
|
467
|
+
state.raw.text = state.text;
|
|
468
|
+
}
|
|
469
|
+
else if (state.type === "thinking") {
|
|
470
|
+
state.raw.thinking = state.thinking;
|
|
471
|
+
if (state.signature) {
|
|
472
|
+
state.raw.signature = state.signature;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function contentPartsToAnthropicBlocks(parts) {
|
|
477
|
+
const blocks = [];
|
|
478
|
+
for (const part of parts) {
|
|
479
|
+
if (part.type === "text") {
|
|
480
|
+
blocks.push({ type: "text", text: part.text });
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
const image = part.image_url.url;
|
|
484
|
+
const dataUrlMatch = image.match(/^data:([^;,]+);base64,(.+)$/);
|
|
485
|
+
if (dataUrlMatch) {
|
|
486
|
+
blocks.push({
|
|
487
|
+
type: "image",
|
|
488
|
+
source: {
|
|
489
|
+
type: "base64",
|
|
490
|
+
media_type: dataUrlMatch[1],
|
|
491
|
+
data: dataUrlMatch[2],
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
blocks.push({ type: "image", source: { type: "url", url: image } });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return blocks;
|
|
500
|
+
}
|
|
501
|
+
function pushAnthropicMessage(messages, next) {
|
|
502
|
+
const last = messages.at(-1);
|
|
503
|
+
if (!last || last.role !== next.role) {
|
|
504
|
+
messages.push(next);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
last.content = mergeAnthropicContent(last.content, next.content);
|
|
508
|
+
}
|
|
509
|
+
function mergeAnthropicContent(current, next) {
|
|
510
|
+
const currentBlocks = typeof current === "string" ? [{ type: "text", text: current }] : current;
|
|
511
|
+
const nextBlocks = typeof next === "string" ? [{ type: "text", text: next }] : next;
|
|
512
|
+
return [...currentBlocks, ...nextBlocks];
|
|
513
|
+
}
|
|
514
|
+
function parseToolInput(raw) {
|
|
515
|
+
try {
|
|
516
|
+
const parsed = JSON.parse(raw || "{}");
|
|
517
|
+
return isObjectRecord(parsed) ? parsed : {};
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
return {};
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function normalizeToolArgs(raw) {
|
|
524
|
+
try {
|
|
525
|
+
JSON.parse(raw);
|
|
526
|
+
return raw;
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
return "{}";
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function isObjectRecord(value) {
|
|
533
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
534
|
+
}
|
|
535
|
+
function extractAnthropicText(content) {
|
|
536
|
+
if (!content)
|
|
537
|
+
return [];
|
|
538
|
+
return content.flatMap((block) => block.type === "text" && typeof block.text === "string" ? [block.text] : []);
|
|
539
|
+
}
|
|
540
|
+
function mergeAnthropicUsage(current, raw) {
|
|
541
|
+
if (!isObjectRecord(raw))
|
|
542
|
+
return current;
|
|
543
|
+
const rawInput = typeof raw.input_tokens === "number" ? raw.input_tokens : undefined;
|
|
544
|
+
const rawCacheRead = typeof raw.cache_read_input_tokens === "number" ? raw.cache_read_input_tokens : undefined;
|
|
545
|
+
const rawCacheCreation = typeof raw.cache_creation_input_tokens === "number" ? raw.cache_creation_input_tokens : undefined;
|
|
546
|
+
const outputTokens = typeof raw.output_tokens === "number" ? raw.output_tokens : current?.completionTokens ?? 0;
|
|
547
|
+
const hasPromptUsage = rawInput !== undefined || rawCacheRead !== undefined || rawCacheCreation !== undefined;
|
|
548
|
+
let promptTokens = current?.promptTokens ?? 0;
|
|
549
|
+
let promptCacheHitTokens = current?.promptCacheHitTokens;
|
|
550
|
+
let promptCacheMissTokens = current?.promptCacheMissTokens;
|
|
551
|
+
let cacheCreationTokens = current?.cacheCreationTokens;
|
|
552
|
+
if (hasPromptUsage) {
|
|
553
|
+
const inputTokens = rawInput ?? promptCacheMissTokens ?? promptTokens;
|
|
554
|
+
const cacheRead = rawCacheRead ?? promptCacheHitTokens ?? 0;
|
|
555
|
+
const cacheCreation = rawCacheCreation ?? 0;
|
|
556
|
+
promptTokens = inputTokens + cacheRead + cacheCreation;
|
|
557
|
+
promptCacheHitTokens = cacheRead;
|
|
558
|
+
promptCacheMissTokens = inputTokens + cacheCreation;
|
|
559
|
+
cacheCreationTokens = cacheCreation;
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
promptTokens,
|
|
563
|
+
completionTokens: outputTokens,
|
|
564
|
+
promptCacheHitTokens,
|
|
565
|
+
promptCacheMissTokens,
|
|
566
|
+
cacheCreationTokens,
|
|
567
|
+
totalTokens: promptTokens + outputTokens,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function shouldEchoThinking(providerId) {
|
|
571
|
+
return providerId?.startsWith("minimax") ?? false;
|
|
572
|
+
}
|
|
573
|
+
function shouldSendBearerAuth(options) {
|
|
574
|
+
return !isOfficialAnthropicBaseUrl(options.baseURL) || options.providerId?.startsWith("minimax") === true;
|
|
575
|
+
}
|
|
576
|
+
function isMiniMaxAnthropicEndpoint(options) {
|
|
577
|
+
const providerId = (options.providerId ?? "").toLowerCase();
|
|
578
|
+
if (providerId !== "minimax" && providerId !== "minimax-anthropic")
|
|
579
|
+
return false;
|
|
580
|
+
try {
|
|
581
|
+
const url = new URL(options.baseURL);
|
|
582
|
+
const host = url.hostname.toLowerCase();
|
|
583
|
+
const path = url.pathname.toLowerCase();
|
|
584
|
+
return (host === "api.minimax.io" || host === "api.minimaxi.com") && path.includes("/anthropic");
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function isOfficialAnthropicBaseUrl(baseURL) {
|
|
591
|
+
try {
|
|
592
|
+
return new URL(baseURL).hostname === "api.anthropic.com";
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
@@ -72,6 +72,7 @@ export function createOpenAICodexProvider(options) {
|
|
|
72
72
|
const body = JSON.stringify(buildRequestBody(messages, {
|
|
73
73
|
model: chatOptions.model,
|
|
74
74
|
tools: chatOptions.tools,
|
|
75
|
+
toolChoice: chatOptions.toolChoice,
|
|
75
76
|
reasoningEffort: requestConfig.reasoningEffort,
|
|
76
77
|
sessionId,
|
|
77
78
|
providerId: options.providerId,
|
|
@@ -314,7 +315,7 @@ function buildRequestBody(messages, options) {
|
|
|
314
315
|
providerId: options.providerId,
|
|
315
316
|
model: options.model,
|
|
316
317
|
}),
|
|
317
|
-
tool_choice: "auto",
|
|
318
|
+
tool_choice: options.tools && options.tools.length > 0 ? options.toolChoice ?? "auto" : undefined,
|
|
318
319
|
parallel_tool_calls: true,
|
|
319
320
|
text: { verbosity: "medium" },
|
|
320
321
|
};
|
|
@@ -454,6 +455,7 @@ function isTransientCodexTransportError(error) {
|
|
|
454
455
|
/\bEPIPE\b/i,
|
|
455
456
|
/socket hang up/i,
|
|
456
457
|
/fetch failed/i,
|
|
458
|
+
/Unable to connect\. Is the computer able to access the url\?/i,
|
|
457
459
|
/unknown certificate verification error/i,
|
|
458
460
|
/certificate (?:verify|verification) (?:failed|error)/i,
|
|
459
461
|
/unable to verify (?:the )?(?:first )?certificate/i,
|
|
@@ -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;
|