@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.
Files changed (51) hide show
  1. package/README.md +24 -0
  2. package/dist/agent/discovery-barrier.d.ts +21 -0
  3. package/dist/agent/discovery-barrier.js +173 -0
  4. package/dist/agent/internal-reminder-sanitizer.d.ts +9 -0
  5. package/dist/agent/internal-reminder-sanitizer.js +198 -0
  6. package/dist/agent/task-classifier.js +23 -5
  7. package/dist/agent.js +215 -30
  8. package/dist/context/budget.js +15 -0
  9. package/dist/context/projector.js +4 -3
  10. package/dist/debug-trace.js +14 -0
  11. package/dist/feishu/serve.js +1 -0
  12. package/dist/main.js +2 -0
  13. package/dist/model-catalog.d.ts +3 -0
  14. package/dist/model-catalog.js +44 -0
  15. package/dist/model-config.d.ts +3 -0
  16. package/dist/model-config.js +3 -0
  17. package/dist/model-pricing.d.ts +3 -2
  18. package/dist/model-pricing.js +8 -0
  19. package/dist/network/chatgpt-transport.d.ts +16 -0
  20. package/dist/network/chatgpt-transport.js +240 -0
  21. package/dist/oauth/openai-codex.d.ts +7 -2
  22. package/dist/oauth/openai-codex.js +7 -4
  23. package/dist/orchestrator/default-hooks.js +13 -2
  24. package/dist/orchestrator/hooks.d.ts +2 -0
  25. package/dist/prompt/compose.js +1 -1
  26. package/dist/prompt/reminders.js +3 -3
  27. package/dist/prompt/runtime.js +1 -0
  28. package/dist/provider-anthropic.d.ts +77 -0
  29. package/dist/provider-anthropic.js +544 -0
  30. package/dist/provider-openai-codex.d.ts +3 -0
  31. package/dist/provider-openai-codex.js +11 -2
  32. package/dist/provider-registry.d.ts +2 -0
  33. package/dist/provider-registry.js +29 -3
  34. package/dist/provider-transform.d.ts +1 -1
  35. package/dist/provider-transform.js +23 -0
  36. package/dist/provider.d.ts +4 -1
  37. package/dist/provider.js +119 -40
  38. package/dist/reasoning-debug.js +4 -1
  39. package/dist/session-log.js +17 -2
  40. package/dist/slash-commands/commands.js +4 -2
  41. package/dist/stats/usage.d.ts +4 -0
  42. package/dist/stats/usage.js +48 -11
  43. package/dist/tools/glob.js +3 -0
  44. package/dist/tools/grep.js +7 -0
  45. package/dist/tui/run.js +22 -12
  46. package/dist/tui-ink/app.js +3 -0
  47. package/dist/tui-ink/message-list.js +6 -3
  48. package/dist/tui-opentui/app.js +3 -0
  49. package/dist/tui-opentui/message-list.js +6 -3
  50. package/dist/types.d.ts +14 -1
  51. 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 fetch(resolveCodexUrl(options.baseURL), buildCodexRequestInit({
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 fetch(resolveRelativeUrl(options.baseURL, path), {
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: cfg.baseURL || builtin?.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}`;