@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
package/dist/agent.js CHANGED
@@ -5,7 +5,7 @@
5
5
  import { compactMessages } from "./context/compact.js";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { compactMessagesWithLLM } from "./context/compact-llm.js";
8
- import { getContextBudget } from "./context/budget.js";
8
+ import { estimateContextTokens, getContextBudget } from "./context/budget.js";
9
9
  import { buildContextUsageSnapshot } from "./context/usage.js";
10
10
  import { isContextOverflowError } from "./context/overflow.js";
11
11
  import { projectMessages } from "./context/projector.js";
@@ -19,6 +19,8 @@ import { getSubtaskPolicy } from "./agent/subtask-policy.js";
19
19
  import { composeAbortSignals } from "./agent/budget-ledger.js";
20
20
  import { assignAgentNickname, builtinAgentProfiles, mergeUsage, selectToolsForAgentProfile, validateAgentProfileTools } from "./agent/profiles.js";
21
21
  import { snapshotSubagentThread, subagentResultFromThread } from "./agent/subagent-control.js";
22
+ import { isHiddenToolResult } from "./agent/discovery-barrier.js";
23
+ import { createStreamingInternalReminderSanitizer, sanitizeAssistantProviderMetadata, sanitizeInternalReminderBlocks } from "./agent/internal-reminder-sanitizer.js";
22
24
  import { buildSystemPrompt } from "./system-prompt.js";
23
25
  import { isOnlyProviderProtocolArtifacts, stripProviderProtocolArtifacts } from "./provider-artifacts.js";
24
26
  import { debugReasoningStream, summarizeDebugText } from "./reasoning-debug.js";
@@ -374,6 +376,8 @@ export class Agent {
374
376
  modelId: this.apiModel,
375
377
  };
376
378
  const streamingToolCalls = new Map();
379
+ const textSanitizer = createStreamingInternalReminderSanitizer();
380
+ const reasoningSanitizer = createStreamingInternalReminderSanitizer();
377
381
  let turnUsage;
378
382
  let assistantAppended = false;
379
383
  currentAssistantMsg = assistantMsg;
@@ -410,6 +414,8 @@ export class Agent {
410
414
  // budget. If it fails (network error, etc.), the projector's existing
411
415
  // algorithmic fallback still kicks in.
412
416
  await this.maybeCompactWithLLM();
417
+ const bufferedStreamingToolCallIds = new Set();
418
+ const discoveryBarrier = hookState.discoveryBarrier;
413
419
  try {
414
420
  const projectedMessages = projectMessages(this.messages, {
415
421
  mode: "budgeted",
@@ -428,6 +434,7 @@ export class Agent {
428
434
  toolCount: toolDefinitions.length,
429
435
  thinkingLevel: this.thinkingLevel,
430
436
  mode: this._mode,
437
+ requestFingerprint: buildProviderRequestFingerprint(projectedMessages, toolDefinitions, this.providerId),
431
438
  }, traceContext);
432
439
  const stream = this.provider.streamChat(projectedMessages, {
433
440
  model: this.apiModel,
@@ -440,28 +447,47 @@ export class Agent {
440
447
  throwIfAborted(abortSignal);
441
448
  switch (chunk.type) {
442
449
  case "text":
443
- assistantMsg.content += chunk.content;
444
- streamTextChars += chunk.content.length;
445
- yield emit({ type: "text_delta", content: chunk.content });
450
+ {
451
+ const sanitizedDelta = textSanitizer.push(chunk.content);
452
+ if (sanitizedDelta) {
453
+ assistantMsg.content += sanitizedDelta;
454
+ streamTextChars += sanitizedDelta.length;
455
+ yield emit({ type: "text_delta", content: sanitizedDelta });
456
+ }
457
+ }
446
458
  break;
447
459
  case "reasoning_delta":
448
- debugReasoningStream({
449
- stage: "agent_receive",
450
- providerId: this._providerId,
451
- modelId: this.apiModel,
452
- turnStep: step,
453
- beforeLength: assistantMsg.reasoning?.length ?? 0,
454
- delta: summarizeDebugText(chunk.content),
455
- afterLength: (assistantMsg.reasoning?.length ?? 0) + chunk.content.length,
456
- });
457
- assistantMsg.reasoning = (assistantMsg.reasoning || "") + chunk.content;
458
- streamReasoningChars += chunk.content.length;
459
- yield emit({ type: "reasoning_delta", content: chunk.content });
460
+ {
461
+ const sanitizedDelta = reasoningSanitizer.push(chunk.content);
462
+ if (sanitizedDelta) {
463
+ debugReasoningStream({
464
+ stage: "agent_receive",
465
+ providerId: this._providerId,
466
+ modelId: this.apiModel,
467
+ turnStep: step,
468
+ beforeLength: assistantMsg.reasoning?.length ?? 0,
469
+ delta: summarizeDebugText(sanitizedDelta),
470
+ afterLength: (assistantMsg.reasoning?.length ?? 0) + sanitizedDelta.length,
471
+ });
472
+ assistantMsg.reasoning = (assistantMsg.reasoning || "") + sanitizedDelta;
473
+ streamReasoningChars += sanitizedDelta.length;
474
+ yield emit({ type: "reasoning_delta", content: sanitizedDelta });
475
+ }
476
+ }
477
+ break;
478
+ case "provider_content_block":
479
+ appendProviderContentBlock(assistantMsg, chunk.provider, chunk.block);
460
480
  break;
461
481
  case "tool_call":
482
+ if (discoveryBarrier?.isEnabled()
483
+ && (bufferedStreamingToolCallIds.has(chunk.id) || discoveryBarrier.shouldBufferStreamingToolCall(chunk.name))) {
484
+ bufferedStreamingToolCallIds.add(chunk.id);
485
+ }
462
486
  if (chunk.isStart) {
463
487
  streamingToolCalls.set(chunk.id, { id: chunk.id, name: chunk.name, args: "" });
464
- yield emit({ type: "tool_call_start", id: chunk.id, name: chunk.name });
488
+ if (!bufferedStreamingToolCallIds.has(chunk.id)) {
489
+ yield emit({ type: "tool_call_start", id: chunk.id, name: chunk.name });
490
+ }
465
491
  }
466
492
  if (!streamingToolCalls.has(chunk.id)) {
467
493
  streamingToolCalls.set(chunk.id, { id: chunk.id, name: chunk.name, args: "" });
@@ -478,13 +504,15 @@ export class Agent {
478
504
  }
479
505
  if (chunk.arguments) {
480
506
  streamToolCallDeltas += 1;
481
- yield emit({
482
- type: "tool_call_delta",
483
- id: currentToolCall.id,
484
- name: currentToolCall.name,
485
- argumentsDelta: chunk.arguments,
486
- arguments: currentToolCall.args,
487
- });
507
+ if (!bufferedStreamingToolCallIds.has(chunk.id)) {
508
+ yield emit({
509
+ type: "tool_call_delta",
510
+ id: currentToolCall.id,
511
+ name: currentToolCall.name,
512
+ argumentsDelta: chunk.arguments,
513
+ arguments: currentToolCall.args,
514
+ });
515
+ }
488
516
  }
489
517
  }
490
518
  if (chunk.isEnd && currentToolCall) {
@@ -494,12 +522,14 @@ export class Agent {
494
522
  arguments: currentToolCall.args,
495
523
  ...(currentToolCall.argsCorrupt ? { argsCorrupt: true } : {}),
496
524
  });
497
- yield emit({
498
- type: "tool_call_end",
499
- id: currentToolCall.id,
500
- name: currentToolCall.name,
501
- arguments: currentToolCall.args,
502
- });
525
+ if (!bufferedStreamingToolCallIds.has(chunk.id)) {
526
+ yield emit({
527
+ type: "tool_call_end",
528
+ id: currentToolCall.id,
529
+ name: currentToolCall.name,
530
+ arguments: currentToolCall.args,
531
+ });
532
+ }
503
533
  streamingToolCalls.delete(chunk.id);
504
534
  }
505
535
  break;
@@ -520,6 +550,27 @@ export class Agent {
520
550
  for (const update of this.drainSubagentToolUpdates())
521
551
  yield emit(update);
522
552
  }
553
+ const flushedText = textSanitizer.flush();
554
+ if (flushedText) {
555
+ assistantMsg.content += flushedText;
556
+ streamTextChars += flushedText.length;
557
+ yield emit({ type: "text_delta", content: flushedText });
558
+ }
559
+ const flushedReasoning = reasoningSanitizer.flush();
560
+ if (flushedReasoning) {
561
+ debugReasoningStream({
562
+ stage: "agent_receive_flush",
563
+ providerId: this._providerId,
564
+ modelId: this.apiModel,
565
+ turnStep: step,
566
+ beforeLength: assistantMsg.reasoning?.length ?? 0,
567
+ delta: summarizeDebugText(flushedReasoning),
568
+ afterLength: (assistantMsg.reasoning?.length ?? 0) + flushedReasoning.length,
569
+ });
570
+ assistantMsg.reasoning = (assistantMsg.reasoning || "") + flushedReasoning;
571
+ streamReasoningChars += flushedReasoning.length;
572
+ yield emit({ type: "reasoning_delta", content: flushedReasoning });
573
+ }
523
574
  traceEvent("provider_stream_end", {
524
575
  elapsedMs: Date.now() - providerStartedAt,
525
576
  textChars: streamTextChars,
@@ -590,6 +641,16 @@ export class Agent {
590
641
  parsedCalls.push({ ...tc, parsedArgs: {}, argsCorrupt: true });
591
642
  }
592
643
  }
644
+ const orderedCalls = hookState.discoveryBarrier?.orderToolCalls(parsedCalls) ?? parsedCalls;
645
+ if (orderedCalls !== parsedCalls) {
646
+ parsedCalls.splice(0, parsedCalls.length, ...orderedCalls);
647
+ assistantMsg.toolCalls = parsedCalls.map((tc) => ({
648
+ id: tc.id,
649
+ name: tc.name,
650
+ arguments: tc.arguments,
651
+ ...(tc.argsCorrupt ? { argsCorrupt: true } : {}),
652
+ }));
653
+ }
593
654
  const executedResults = [];
594
655
  const appendCancelledToolMessages = (startIndex) => {
595
656
  for (let pendingIndex = startIndex; pendingIndex < parsedCalls.length; pendingIndex++) {
@@ -634,6 +695,51 @@ export class Agent {
634
695
  arguments: tc.arguments,
635
696
  };
636
697
  flushGovernorReminders();
698
+ if (bufferedStreamingToolCallIds.has(tc.id) && !isHiddenToolResult(blockedResult)) {
699
+ yield emit({ type: "tool_call_start", id: tc.id, name: tc.name });
700
+ if (tc.arguments) {
701
+ yield emit({
702
+ type: "tool_call_delta",
703
+ id: tc.id,
704
+ name: tc.name,
705
+ argumentsDelta: tc.arguments,
706
+ arguments: tc.arguments,
707
+ });
708
+ }
709
+ yield emit({ type: "tool_call_end", id: tc.id, name: tc.name, arguments: tc.arguments });
710
+ }
711
+ if (isHiddenToolResult(blockedResult)) {
712
+ let result = blockedResult;
713
+ await hookBus.runAfterToolCall({
714
+ agent: this,
715
+ cwd,
716
+ input: userInput,
717
+ state: hookState,
718
+ queueReminder,
719
+ flushReminders: flushGovernorReminders,
720
+ toolCall: tc,
721
+ result,
722
+ replaceResult: (next) => {
723
+ result = next;
724
+ },
725
+ });
726
+ traceEvent("speculative_read_blocked", {
727
+ id: tc.id,
728
+ name: tc.name,
729
+ args: summarizeTraceValue(tc.parsedArgs),
730
+ result: summarizeTraceToolResult(result),
731
+ }, traceContext);
732
+ this.appendMessage({
733
+ role: "tool",
734
+ toolCallId: tc.id,
735
+ content: result.content,
736
+ metadata: result.metadata,
737
+ isError: result.isError,
738
+ });
739
+ executedResults.push(result);
740
+ flushGovernorReminders();
741
+ continue;
742
+ }
637
743
  const toolStartedAt = Date.now();
638
744
  traceEvent("tool_execute_start", {
639
745
  id: tc.id,
@@ -1435,6 +1541,15 @@ export class Agent {
1435
1541
  }
1436
1542
  }
1437
1543
  appendMessage(message) {
1544
+ if (message.role === "assistant" && message.content) {
1545
+ message.content = sanitizeInternalReminderBlocks(message.content);
1546
+ }
1547
+ if (message.role === "assistant" && message.reasoning) {
1548
+ message.reasoning = sanitizeInternalReminderBlocks(message.reasoning);
1549
+ }
1550
+ if (message.role === "assistant" && message.providerMetadata) {
1551
+ message.providerMetadata = sanitizeAssistantProviderMetadata(message.providerMetadata);
1552
+ }
1438
1553
  this.messages.push(message);
1439
1554
  traceEvent("agent_message_append", {
1440
1555
  message: summarizeTraceMessage(message),
@@ -1589,6 +1704,76 @@ function estimateResidentChars(messages) {
1589
1704
  }
1590
1705
  return total;
1591
1706
  }
1707
+ function appendProviderContentBlock(message, provider, block) {
1708
+ if (provider !== "anthropic")
1709
+ return;
1710
+ const current = message.providerMetadata?.anthropic?.contentBlocks ?? [];
1711
+ message.providerMetadata = {
1712
+ ...message.providerMetadata,
1713
+ anthropic: {
1714
+ ...message.providerMetadata?.anthropic,
1715
+ contentBlocks: [...current, cloneProviderRawContentBlock(block)],
1716
+ },
1717
+ };
1718
+ }
1719
+ function buildProviderRequestFingerprint(messages, tools, providerId) {
1720
+ const roleCounts = {};
1721
+ let contentChars = 0;
1722
+ let reasoningChars = 0;
1723
+ let toolResultChars = 0;
1724
+ let maxToolResultChars = 0;
1725
+ let assistantToolCalls = 0;
1726
+ let rawAnthropicBlocks = 0;
1727
+ let rawAnthropicThinkingBlocks = 0;
1728
+ let rawAnthropicSignatureChars = 0;
1729
+ for (const message of messages) {
1730
+ roleCounts[message.role] = (roleCounts[message.role] ?? 0) + 1;
1731
+ if (message.role === "assistant") {
1732
+ contentChars += message.content.length;
1733
+ reasoningChars += message.reasoning?.length ?? 0;
1734
+ assistantToolCalls += message.toolCalls?.length ?? 0;
1735
+ const blocks = message.providerMetadata?.anthropic?.contentBlocks ?? [];
1736
+ rawAnthropicBlocks += blocks.length;
1737
+ for (const block of blocks) {
1738
+ if (block.type === "thinking" || block.type === "redacted_thinking") {
1739
+ rawAnthropicThinkingBlocks += 1;
1740
+ }
1741
+ if (typeof block.signature === "string") {
1742
+ rawAnthropicSignatureChars += block.signature.length;
1743
+ }
1744
+ }
1745
+ }
1746
+ else if (message.role === "tool") {
1747
+ toolResultChars += message.content.length;
1748
+ maxToolResultChars = Math.max(maxToolResultChars, message.content.length);
1749
+ }
1750
+ else if (message.role === "user") {
1751
+ contentChars += typeof message.content === "string"
1752
+ ? message.content.length
1753
+ : message.content.reduce((sum, part) => sum + (part.type === "text" ? part.text.length : part.image_url.url.length), 0);
1754
+ }
1755
+ else {
1756
+ contentChars += message.content.length;
1757
+ }
1758
+ }
1759
+ return {
1760
+ roleCounts,
1761
+ estimatedTokens: estimateContextTokens(messages, providerId),
1762
+ projectedJsonBytes: Buffer.byteLength(JSON.stringify(messages), "utf8"),
1763
+ toolSchemaJsonBytes: Buffer.byteLength(JSON.stringify(tools), "utf8"),
1764
+ contentChars,
1765
+ reasoningChars,
1766
+ toolResultChars,
1767
+ maxToolResultChars,
1768
+ assistantToolCalls,
1769
+ rawAnthropicBlocks,
1770
+ rawAnthropicThinkingBlocks,
1771
+ rawAnthropicSignatureChars,
1772
+ };
1773
+ }
1774
+ function cloneProviderRawContentBlock(block) {
1775
+ return JSON.parse(JSON.stringify(block));
1776
+ }
1592
1777
  function throwIfAborted(signal) {
1593
1778
  if (!signal?.aborted)
1594
1779
  return;
@@ -20,6 +20,7 @@ export function estimateMessageTokens(message, providerId) {
20
20
  case "assistant":
21
21
  return estimate(message.content)
22
22
  + estimate(message.reasoning ?? "")
23
+ + estimateProviderMetadataOverhead(message.providerMetadata, providerId)
23
24
  + (message.toolCalls?.reduce((sum, toolCall) => sum + estimate(toolCall.arguments) + 12, 0) ?? 0)
24
25
  + 8;
25
26
  case "user":
@@ -34,6 +35,20 @@ export function estimateMessageTokens(message, providerId) {
34
35
  }, 8);
35
36
  }
36
37
  }
38
+ function estimateProviderMetadataOverhead(metadata, providerId) {
39
+ const blocks = metadata?.anthropic?.contentBlocks;
40
+ if (!blocks || blocks.length === 0)
41
+ return 0;
42
+ const estimate = (text) => estimateTextTokens(text, providerId);
43
+ return blocks.reduce((sum, block) => {
44
+ let overhead = 0;
45
+ if (typeof block.signature === "string")
46
+ overhead += estimate(block.signature);
47
+ if (block.type === "redacted_thinking" && typeof block.data === "string")
48
+ overhead += estimate(block.data);
49
+ return sum + overhead;
50
+ }, 0);
51
+ }
37
52
  export function estimateContextTokens(messages, providerId) {
38
53
  return messages.reduce((sum, message) => sum + estimateMessageTokens(message, providerId), 0);
39
54
  }
@@ -1,6 +1,7 @@
1
1
  import { getContextBudget } from "./budget.js";
2
2
  import { compactCurrentTurnToolGroups, compactMessages } from "./compact.js";
3
3
  import { pruneMessages } from "./prune.js";
4
+ import { formatInternalContextBlock, formatInternalReminderBlock } from "../agent/internal-reminder-sanitizer.js";
4
5
  // Prefix-cache invariant: only the leading static system prompt is promoted to
5
6
  // the first provider message. Runtime meta reminders stay in the conversational
6
7
  // body at their original relative position, so a new per-turn reminder does not
@@ -166,14 +167,14 @@ function isEmptyAssistantMessage(message) {
166
167
  function formatMetaMessage(message) {
167
168
  switch (message.kind) {
168
169
  case "system-reminder":
169
- return `Runtime reminder:\n${message.content}`;
170
+ return formatInternalReminderBlock(message.kind, message.content);
170
171
  case "runtime-context":
171
172
  default:
172
- return `Runtime context:\n${message.content}`;
173
+ return formatInternalContextBlock(message.kind, message.content);
173
174
  }
174
175
  }
175
176
  function formatRuntimeSystemMessage(message) {
176
- return `Runtime context:\n${message.content}`;
177
+ return formatInternalContextBlock("runtime-system", message.content);
177
178
  }
178
179
  function cloneMessage(message) {
179
180
  if (message.role === "assistant") {
@@ -112,6 +112,7 @@ export function summarizeTraceMessage(message) {
112
112
  content: summarizeTraceText(message.content),
113
113
  reasoning: summarizeTraceText(message.reasoning ?? ""),
114
114
  error: message.error,
115
+ providerMetadata: summarizeAssistantProviderMetadata(message),
115
116
  toolCalls: message.toolCalls?.map((call) => ({
116
117
  id: call.id,
117
118
  name: call.name,
@@ -141,6 +142,19 @@ export function summarizeTraceMessage(message) {
141
142
  content: summarizeTraceText(message.content),
142
143
  };
143
144
  }
145
+ function summarizeAssistantProviderMetadata(message) {
146
+ const blocks = message.providerMetadata?.anthropic?.contentBlocks;
147
+ if (!blocks || blocks.length === 0)
148
+ return undefined;
149
+ return {
150
+ anthropic: {
151
+ contentBlocks: blocks.length,
152
+ thinkingBlocks: blocks.filter((block) => block.type === "thinking" || block.type === "redacted_thinking").length,
153
+ signatureChars: blocks.reduce((sum, block) => sum + (typeof block.signature === "string" ? block.signature.length : 0), 0),
154
+ types: blocks.map((block) => block.type).slice(0, 32),
155
+ },
156
+ };
157
+ }
144
158
  export function summarizeTraceToolResult(result) {
145
159
  return {
146
160
  content: summarizeTraceText(result.content),
@@ -96,6 +96,7 @@ export async function serveFeishu(opts = {}) {
96
96
  apiKey,
97
97
  baseURL,
98
98
  promptCacheKey,
99
+ protocol: providerRegistry.getConfigured().find((provider) => provider.id === providerId)?.protocol,
99
100
  openAICodexAuth: providerRegistry.createOpenAICodexAuthAdapter(providerId),
100
101
  });
101
102
  const createProviderForRoute = async (route, promptCacheKey) => {
package/dist/main.js CHANGED
@@ -83,6 +83,7 @@ async function main() {
83
83
  baseURL: defaultProvider.baseURL,
84
84
  thinkingLevel: args.thinkingLevel,
85
85
  promptCacheKey: sessionPromptCacheKey,
86
+ protocol: defaultProvider.protocol,
86
87
  openAICodexAuth: registry.createOpenAICodexAuthAdapter(defaultProvider.id),
87
88
  })
88
89
  : createUnavailableProvider(unavailableProviderMessage);
@@ -92,6 +93,7 @@ async function main() {
92
93
  baseURL,
93
94
  thinkingLevel: args.thinkingLevel,
94
95
  promptCacheKey: sessionPromptCacheKey,
96
+ protocol: registry.getConfigured().find((provider) => provider.id === providerId)?.protocol,
95
97
  openAICodexAuth: registry.createOpenAICodexAuthAdapter(providerId),
96
98
  });
97
99
  const createProviderForRoute = async (route) => {
@@ -1,8 +1,11 @@
1
1
  import type { ReasoningEffort } from "./types.js";
2
+ export type ProviderProtocol = "openai-chat" | "anthropic-messages";
2
3
  export interface BuiltinProviderDefinition {
3
4
  id: string;
4
5
  name: string;
5
6
  baseURL: string;
7
+ protocol?: ProviderProtocol;
8
+ hidden?: boolean;
6
9
  supportsOAuth?: boolean;
7
10
  }
8
11
  export interface BuiltinModelDefinition {
@@ -2,6 +2,7 @@ export const BUILTIN_PROVIDERS = [
2
2
  { id: "openrouter", name: "OpenRouter", baseURL: "https://openrouter.ai/api/v1" },
3
3
  { id: "openai", name: "OpenAI", baseURL: "https://api.openai.com/v1", supportsOAuth: true },
4
4
  { id: "openai-codex", name: "OpenAI Codex (ChatGPT)", baseURL: "https://chatgpt.com/backend-api" },
5
+ { id: "anthropic", name: "Anthropic", baseURL: "https://api.anthropic.com", protocol: "anthropic-messages" },
5
6
  { id: "deepseek", name: "DeepSeek", baseURL: "https://api.deepseek.com" },
6
7
  { id: "google", name: "Google", baseURL: "https://generativelanguage.googleapis.com/v1beta/openai" },
7
8
  { id: "zhipuai", name: "Zhipu AI", baseURL: "https://open.bigmodel.cn/api/paas/v4" },
@@ -9,6 +10,10 @@ export const BUILTIN_PROVIDERS = [
9
10
  { id: "zai", name: "Z.AI", baseURL: "https://api.z.ai/api/paas/v4" },
10
11
  { id: "zai-coding-plan", name: "Z.AI Coding Plan", baseURL: "https://api.z.ai/api/coding/paas/v4" },
11
12
  { id: "alibaba", name: "Alibaba DashScope", baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1" },
13
+ { id: "minimax", name: "MiniMax Token Plan", baseURL: "https://api.minimaxi.com/anthropic", protocol: "anthropic-messages" },
14
+ { id: "minimax-openai", name: "MiniMax (OpenAI Compatible)", baseURL: "https://api.minimaxi.com/v1" },
15
+ { id: "minimax-anthropic", name: "MiniMax (Anthropic Compatible)", baseURL: "https://api.minimaxi.com/anthropic", protocol: "anthropic-messages", hidden: true },
16
+ { id: "stepfun", name: "StepFun Step Plan", baseURL: "https://api.stepfun.com/step_plan/v1" },
12
17
  { id: "moonshot-cn", name: "Moonshot (国内 platform.moonshot.cn)", baseURL: "https://api.moonshot.cn/v1" },
13
18
  { id: "moonshot-intl", name: "Moonshot (海外 platform.moonshot.ai)", baseURL: "https://api.moonshot.ai/v1" },
14
19
  { id: "kimi-for-coding", name: "Kimi for Coding", baseURL: "https://api.kimi.com/coding/v1" },
@@ -24,6 +29,11 @@ const GPT51_CODEX_MINI_LEVELS = ["off", "medium", "high"];
24
29
  const OPENAI_CHAT_LEVELS = ["off"];
25
30
  const TOGGLE_THINKING_LEVELS = ["off", "medium"];
26
31
  const DEEPSEEK_V4_LEVELS = ["high", "max"];
32
+ const STEPFUN_REASONING_LEVELS = ["off", "low", "medium", "high"];
33
+ const MINIMAX_M3_REASONING_LEVELS = ["off", "medium"];
34
+ const MINIMAX_REASONING_LEVELS = ["medium"];
35
+ const ANTHROPIC_ADAPTIVE_LEVELS = ["off", "medium"];
36
+ const ANTHROPIC_CHAT_LEVELS = ["off"];
27
37
  export const BUILTIN_MODELS = [
28
38
  { id: "gpt-5.5", name: "gpt-5.5", providerId: "openai-codex", reasoningLevels: ALL_OPENAI_LEVELS, contextWindow: 272000, toolOutputTokenLimit: 10000 },
29
39
  { id: "gpt-5.4", name: "gpt-5.4", providerId: "openai-codex", reasoningLevels: ALL_OPENAI_LEVELS, contextWindow: 272000 },
@@ -40,6 +50,9 @@ export const BUILTIN_MODELS = [
40
50
  { id: "o1-preview", name: "o1-preview", providerId: "openai", reasoningLevels: ["off", "low", "medium", "high"], contextWindow: 128000 },
41
51
  { id: "o1-mini", name: "o1-mini", providerId: "openai", reasoningLevels: ["off", "low", "medium", "high"], contextWindow: 128000 },
42
52
  { id: "gpt-4-turbo", name: "gpt-4-turbo", providerId: "openai", reasoningLevels: OPENAI_CHAT_LEVELS, contextWindow: 128000 },
53
+ { id: "claude-opus-4-8", name: "Claude Opus 4.8", providerId: "anthropic", reasoningLevels: ANTHROPIC_ADAPTIVE_LEVELS, contextWindow: 1000000 },
54
+ { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", providerId: "anthropic", reasoningLevels: ANTHROPIC_ADAPTIVE_LEVELS, contextWindow: 1000000 },
55
+ { id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", providerId: "anthropic", reasoningLevels: ANTHROPIC_CHAT_LEVELS, contextWindow: 200000 },
43
56
  { id: "deepseek-v4-flash", name: "deepseek-v4-flash", providerId: "deepseek", reasoningLevels: DEEPSEEK_V4_LEVELS, contextWindow: 1048576 },
44
57
  { id: "deepseek-v4-pro", name: "deepseek-v4-pro", providerId: "deepseek", reasoningLevels: DEEPSEEK_V4_LEVELS, contextWindow: 1048576 },
45
58
  { id: "gemini-2.5-pro-preview-03-25", name: "gemini-2.5-pro-preview-03-25", providerId: "google", reasoningLevels: ["off", "low", "high"], contextWindow: 128000 },
@@ -59,6 +72,37 @@ export const BUILTIN_MODELS = [
59
72
  { id: "glm-4.6", name: "GLM-4.6", providerId: "zai-coding-plan", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 200000 },
60
73
  { id: "qwen3.6-plus", name: "Qwen3.6 Plus", providerId: "alibaba", reasoningLevels: ["off"], contextWindow: 1048576 },
61
74
  { id: "qwen3.7-max", name: "Qwen3.7 Max", providerId: "alibaba", reasoningLevels: ["off"], contextWindow: 1048576 },
75
+ { id: "MiniMax-M3", name: "MiniMax M3", providerId: "minimax", reasoningLevels: MINIMAX_M3_REASONING_LEVELS, contextWindow: 1000000 },
76
+ { id: "MiniMax-M2.7", name: "MiniMax M2.7", providerId: "minimax", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
77
+ { id: "MiniMax-M2.7-highspeed", name: "MiniMax M2.7 Highspeed", providerId: "minimax", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
78
+ { id: "MiniMax-M2.5", name: "MiniMax M2.5", providerId: "minimax", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
79
+ { id: "MiniMax-M2.5-highspeed", name: "MiniMax M2.5 Highspeed", providerId: "minimax", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
80
+ { id: "MiniMax-M2.1", name: "MiniMax M2.1", providerId: "minimax", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
81
+ { id: "MiniMax-M2.1-highspeed", name: "MiniMax M2.1 Highspeed", providerId: "minimax", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
82
+ { id: "MiniMax-M2", name: "MiniMax M2", providerId: "minimax", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
83
+ { id: "M2-her", name: "M2-her", providerId: "minimax", reasoningLevels: ["off"], contextWindow: 64000 },
84
+ { id: "MiniMax-M3", name: "MiniMax M3", providerId: "minimax-openai", reasoningLevels: MINIMAX_M3_REASONING_LEVELS, contextWindow: 1000000 },
85
+ { id: "MiniMax-M2.7", name: "MiniMax M2.7", providerId: "minimax-openai", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
86
+ { id: "MiniMax-M2.7-highspeed", name: "MiniMax M2.7 Highspeed", providerId: "minimax-openai", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
87
+ { id: "MiniMax-M2.5", name: "MiniMax M2.5", providerId: "minimax-openai", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
88
+ { id: "MiniMax-M2.5-highspeed", name: "MiniMax M2.5 Highspeed", providerId: "minimax-openai", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
89
+ { id: "MiniMax-M2.1", name: "MiniMax M2.1", providerId: "minimax-openai", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
90
+ { id: "MiniMax-M2.1-highspeed", name: "MiniMax M2.1 Highspeed", providerId: "minimax-openai", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
91
+ { id: "MiniMax-M2", name: "MiniMax M2", providerId: "minimax-openai", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
92
+ { id: "M2-her", name: "M2-her", providerId: "minimax-openai", reasoningLevels: ["off"], contextWindow: 64000 },
93
+ { id: "MiniMax-M3", name: "MiniMax M3", providerId: "minimax-anthropic", reasoningLevels: MINIMAX_M3_REASONING_LEVELS, contextWindow: 1000000 },
94
+ { id: "MiniMax-M2.7", name: "MiniMax M2.7", providerId: "minimax-anthropic", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
95
+ { id: "MiniMax-M2.7-highspeed", name: "MiniMax M2.7 Highspeed", providerId: "minimax-anthropic", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
96
+ { id: "MiniMax-M2.5", name: "MiniMax M2.5", providerId: "minimax-anthropic", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
97
+ { id: "MiniMax-M2.5-highspeed", name: "MiniMax M2.5 Highspeed", providerId: "minimax-anthropic", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
98
+ { id: "MiniMax-M2.1", name: "MiniMax M2.1", providerId: "minimax-anthropic", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
99
+ { id: "MiniMax-M2.1-highspeed", name: "MiniMax M2.1 Highspeed", providerId: "minimax-anthropic", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
100
+ { id: "MiniMax-M2", name: "MiniMax M2", providerId: "minimax-anthropic", reasoningLevels: MINIMAX_REASONING_LEVELS, contextWindow: 204800 },
101
+ { id: "M2-her", name: "M2-her", providerId: "minimax-anthropic", reasoningLevels: ["off"], contextWindow: 64000 },
102
+ { id: "step-3.7-flash", name: "Step 3.7 Flash", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS, contextWindow: 256000 },
103
+ { id: "step-3.5-flash-2603", name: "Step 3.5 Flash 2603", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
104
+ { id: "step-3.5-flash", name: "Step 3.5 Flash", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
105
+ { id: "step-router-v1", name: "Step Router V1", providerId: "stepfun", reasoningLevels: STEPFUN_REASONING_LEVELS },
62
106
  { id: "kimi-k2.6", name: "Kimi K2.6", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
63
107
  { id: "k2.6-code-preview", name: "Kimi K2.6 Code Preview", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
64
108
  { id: "kimi-k2.5", name: "Kimi K2.5", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
@@ -4,10 +4,12 @@
4
4
  * Users can define providers, API keys, base URLs, and custom models
5
5
  * in ~/.bubble/models.json.
6
6
  */
7
+ import type { ProviderProtocol } from "./model-catalog.js";
7
8
  import type { ModelInfo } from "./provider-registry.js";
8
9
  export interface ProviderModelConfig {
9
10
  baseURL?: string;
10
11
  apiKey?: string;
12
+ protocol?: ProviderProtocol;
11
13
  models?: Array<{
12
14
  id: string;
13
15
  name?: string;
@@ -29,4 +31,5 @@ export declare class ModelConfig {
29
31
  getCustomModels(providerId: string): ModelInfo[];
30
32
  getApiKey(providerId: string): string | undefined;
31
33
  getBaseURL(providerId: string): string | undefined;
34
+ getProtocol(providerId: string): ProviderProtocol | undefined;
32
35
  }
@@ -56,4 +56,7 @@ export class ModelConfig {
56
56
  getBaseURL(providerId) {
57
57
  return this.data?.providers?.[providerId]?.baseURL;
58
58
  }
59
+ getProtocol(providerId) {
60
+ return this.data?.providers?.[providerId]?.protocol;
61
+ }
59
62
  }
@@ -1,8 +1,9 @@
1
1
  import type { TokenUsage } from "./types.js";
2
+ export type PricingCurrency = "USD" | "CNY";
2
3
  export interface ModelPricing {
3
4
  providerId: string;
4
5
  modelId: string;
5
- currency: "USD";
6
+ currency: PricingCurrency;
6
7
  inputCacheHitPerMillion: number;
7
8
  inputCacheMissPerMillion: number;
8
9
  outputPerMillion: number;
@@ -14,7 +15,7 @@ export interface ModelPricing {
14
15
  };
15
16
  }
16
17
  export interface UsageCost {
17
- currency: "USD";
18
+ currency: PricingCurrency;
18
19
  cost: number;
19
20
  estimated: boolean;
20
21
  }
@@ -21,6 +21,14 @@ export const MODEL_PRICING = [
21
21
  outputPerMillion: 3.48,
22
22
  },
23
23
  },
24
+ {
25
+ providerId: "stepfun",
26
+ modelId: "step-3.7-flash",
27
+ currency: "CNY",
28
+ inputCacheHitPerMillion: 0.27,
29
+ inputCacheMissPerMillion: 1.35,
30
+ outputPerMillion: 8.1,
31
+ },
24
32
  ];
25
33
  export function getModelPricing(providerId, modelId) {
26
34
  return MODEL_PRICING.find((item) => item.providerId === providerId && item.modelId === modelId);
@@ -0,0 +1,16 @@
1
+ import { type Dispatcher } from "undici";
2
+ export type ChatGptFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
3
+ export interface ChatGptFetchOptions {
4
+ fetch?: ChatGptFetch;
5
+ env?: NodeJS.ProcessEnv;
6
+ }
7
+ type RequestInitWithDispatcher = RequestInit & {
8
+ dispatcher?: Dispatcher;
9
+ };
10
+ export declare function chatGptFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
11
+ export declare function getChatGptFetch(env?: NodeJS.ProcessEnv): ChatGptFetch;
12
+ export declare function createChatGptFetch(options?: ChatGptFetchOptions): ChatGptFetch;
13
+ export declare function createChatGptDispatcher(env?: NodeJS.ProcessEnv, input?: RequestInfo | URL): Dispatcher | undefined;
14
+ export declare function withChatGptNetworkOptions(input: RequestInfo | URL, init: RequestInit | undefined, env?: NodeJS.ProcessEnv, dispatcher?: Dispatcher | undefined): RequestInitWithDispatcher;
15
+ export declare function normalizeChatGptNetworkError(error: unknown, env?: NodeJS.ProcessEnv): Error;
16
+ export {};