@bubblebrain-ai/bubble 0.0.16 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/provider.js CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import OpenAI from "openai";
7
7
  import { appendFileSync } from "node:fs";
8
+ import { createAnthropicMessagesProvider } from "./provider-anthropic.js";
8
9
  import { createOpenAICodexProvider, isOpenAICodexBaseUrl } from "./provider-openai-codex.js";
9
10
  import { createProviderProtocolArtifactFilter } from "./provider-artifacts.js";
10
11
  import { resolveProviderRequestConfig } from "./provider-transform.js";
@@ -36,6 +37,15 @@ export function toChatCompletionsMessage(message, options = {}) {
36
37
  // provider field, even when the original value is an empty string.
37
38
  out.reasoning_content = message.reasoning ?? "";
38
39
  }
40
+ if (reasoningContentEcho === "minimax" && message.reasoning) {
41
+ out.reasoning_details = [{
42
+ type: "reasoning.text",
43
+ id: "reasoning-text-1",
44
+ format: "MiniMax-response-v1",
45
+ index: 0,
46
+ text: message.reasoning,
47
+ }];
48
+ }
39
49
  if (message.toolCalls && message.toolCalls.length > 0) {
40
50
  out.tool_calls = message.toolCalls.map((tc) => ({
41
51
  id: tc.id,
@@ -65,6 +75,9 @@ export function createUnavailableProvider(message) {
65
75
  return { streamChat, complete };
66
76
  }
67
77
  export function createProviderInstance(options) {
78
+ if (resolveProviderProtocol(options) === "anthropic-messages") {
79
+ return createAnthropicMessagesProvider(options);
80
+ }
68
81
  if (isOpenAICodexBaseUrl(options.baseURL)) {
69
82
  return createOpenAICodexProvider({
70
83
  ...options,
@@ -96,8 +109,8 @@ export function createProviderInstance(options) {
96
109
  tool_choice: tools && tools.length > 0 ? "auto" : undefined,
97
110
  stream: true,
98
111
  };
99
- // DeepSeek only emits final usage in streaming mode when this flag is set.
100
- if (options.providerId === "deepseek") {
112
+ // DeepSeek and MiniMax only emit final usage in streaming mode when this flag is set.
113
+ if (options.providerId === "deepseek" || isMiniMaxOpenAICompatible(options)) {
101
114
  body.stream_options = { include_usage: true };
102
115
  }
103
116
  if (!requestConfig.omitTemperature) {
@@ -121,6 +134,7 @@ export function createProviderInstance(options) {
121
134
  yield* translateOpenAIStream(stream, {
122
135
  toolArgsMergeMode: resolveToolArgsMergeMode(options.providerId || "", options.baseURL),
123
136
  reasoningMergeMode: resolveReasoningMergeMode(options.providerId || "", options.baseURL),
137
+ textMergeMode: resolveTextMergeMode(options.providerId || "", options.baseURL),
124
138
  debugProviderId: options.providerId || "",
125
139
  debugModelId: chatOptions.model,
126
140
  });
@@ -153,6 +167,26 @@ export function createProviderInstance(options) {
153
167
  }
154
168
  return { streamChat, complete };
155
169
  }
170
+ function resolveProviderProtocol(options) {
171
+ if (options.protocol)
172
+ return options.protocol;
173
+ const providerId = (options.providerId || "").toLowerCase();
174
+ const baseURL = options.baseURL.toLowerCase();
175
+ if (providerId === "anthropic"
176
+ || providerId.endsWith("-anthropic")
177
+ || baseURL.includes("/anthropic")) {
178
+ return "anthropic-messages";
179
+ }
180
+ return "openai-chat";
181
+ }
182
+ function isMiniMaxOpenAICompatible(options) {
183
+ const providerId = (options.providerId || "").toLowerCase();
184
+ const baseURL = options.baseURL.toLowerCase();
185
+ return providerId === "minimax-openai"
186
+ || (providerId === "minimax" && !baseURL.includes("/anthropic"))
187
+ || baseURL.includes("api.minimaxi.com/v1")
188
+ || baseURL.includes("api.minimax.io/v1");
189
+ }
156
190
  export function normalizeToolArgsDetailed(raw) {
157
191
  const s = (raw ?? "").trim();
158
192
  if (!s) {
@@ -210,6 +244,15 @@ function resolveReasoningMergeMode(providerId, baseURL) {
210
244
  const url = baseURL.toLowerCase();
211
245
  if (id === "fireworks" || url.includes("fireworks.ai"))
212
246
  return "snapshot";
247
+ if (id === "minimax" || url.includes("api.minimaxi.com") || url.includes("api.minimax.io"))
248
+ return "snapshot";
249
+ return "delta";
250
+ }
251
+ function resolveTextMergeMode(providerId, baseURL) {
252
+ const id = providerId.toLowerCase();
253
+ const url = baseURL.toLowerCase();
254
+ if (id === "minimax" || url.includes("api.minimaxi.com") || url.includes("api.minimax.io"))
255
+ return "snapshot";
213
256
  return "delta";
214
257
  }
215
258
  function extractBalancedJson(s, start) {
@@ -257,7 +300,9 @@ export async function* translateOpenAIStream(stream, options = {}) {
257
300
  const textFilter = createProviderProtocolArtifactFilter();
258
301
  const toolArgsMergeMode = options.toolArgsMergeMode ?? "delta";
259
302
  const reasoningMergeMode = options.reasoningMergeMode ?? "delta";
303
+ const textMergeMode = options.textMergeMode ?? "delta";
260
304
  let reasoningBuffer = "";
305
+ let textBuffer = "";
261
306
  let rawChunkSeq = 0;
262
307
  // DeepSeek (and some inference re-hosts) sometimes deliver reasoning twice:
263
308
  // once via a dedicated `reasoning_content` / `thinking` field, and again
@@ -318,6 +363,7 @@ export async function* translateOpenAIStream(stream, options = {}) {
318
363
  reasoning: summarizeDebugText(delta?.reasoning),
319
364
  thinking: summarizeDebugText(delta?.thinking),
320
365
  reasoningContent: summarizeDebugText(delta?.reasoning_content),
366
+ reasoningDetails: summarizeDebugText(extractReasoningDetailsText(delta?.reasoning_details)),
321
367
  });
322
368
  if (usage) {
323
369
  yield {
@@ -325,8 +371,16 @@ export async function* translateOpenAIStream(stream, options = {}) {
325
371
  usage: {
326
372
  promptTokens: typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : 0,
327
373
  completionTokens: typeof usage.completion_tokens === "number" ? usage.completion_tokens : 0,
328
- promptCacheHitTokens: typeof usage.prompt_cache_hit_tokens === "number" ? usage.prompt_cache_hit_tokens : undefined,
329
- promptCacheMissTokens: typeof usage.prompt_cache_miss_tokens === "number" ? usage.prompt_cache_miss_tokens : undefined,
374
+ promptCacheHitTokens: typeof usage.prompt_cache_hit_tokens === "number"
375
+ ? usage.prompt_cache_hit_tokens
376
+ : typeof usage.prompt_tokens_details?.cached_tokens === "number"
377
+ ? usage.prompt_tokens_details.cached_tokens
378
+ : undefined,
379
+ promptCacheMissTokens: typeof usage.prompt_cache_miss_tokens === "number"
380
+ ? usage.prompt_cache_miss_tokens
381
+ : typeof usage.prompt_tokens_details?.cached_tokens === "number" && typeof usage.prompt_tokens === "number"
382
+ ? Math.max(0, usage.prompt_tokens - usage.prompt_tokens_details.cached_tokens)
383
+ : undefined,
330
384
  reasoningTokens: typeof usage.completion_tokens_details?.reasoning_tokens === "number"
331
385
  ? usage.completion_tokens_details.reasoning_tokens
332
386
  : undefined,
@@ -334,14 +388,21 @@ export async function* translateOpenAIStream(stream, options = {}) {
334
388
  },
335
389
  };
336
390
  }
337
- const reasoningField = delta?.reasoning !== undefined
338
- ? "reasoning"
339
- : delta?.thinking !== undefined
340
- ? "thinking"
341
- : delta?.reasoning_content !== undefined
342
- ? "reasoning_content"
343
- : undefined;
344
- const reasoning = reasoningField ? delta[reasoningField] : undefined;
391
+ const reasoningDetails = extractReasoningDetailsText(delta?.reasoning_details);
392
+ const reasoningField = reasoningDetails !== undefined
393
+ ? "reasoning_details"
394
+ : delta?.reasoning !== undefined
395
+ ? "reasoning"
396
+ : delta?.thinking !== undefined
397
+ ? "thinking"
398
+ : delta?.reasoning_content !== undefined
399
+ ? "reasoning_content"
400
+ : undefined;
401
+ const reasoning = reasoningDetails !== undefined
402
+ ? reasoningDetails
403
+ : reasoningField
404
+ ? delta[reasoningField]
405
+ : undefined;
345
406
  if (reasoning) {
346
407
  hasDedicatedReasoningChannel = true;
347
408
  const merged = mergeStreamingText(reasoningBuffer, reasoning, reasoningMergeMode);
@@ -362,36 +423,41 @@ export async function* translateOpenAIStream(stream, options = {}) {
362
423
  }
363
424
  }
364
425
  if (delta?.content) {
365
- const thinkMatch = delta.content.match(/<think>([\s\S]*?)<\/think>/);
366
- if (thinkMatch) {
367
- if (thinkMatch[1] && !hasDedicatedReasoningChannel) {
368
- const merged = mergeStreamingText(reasoningBuffer, thinkMatch[1], reasoningMergeMode);
369
- reasoningBuffer = merged.args;
370
- debugReasoningStream({
371
- stage: "provider_emit",
372
- providerId: options.debugProviderId,
373
- modelId: options.debugModelId,
374
- chunkSeq: rawChunkSeq,
375
- source: "content_think",
376
- mergeMode: reasoningMergeMode,
377
- suppressed: !merged.delta,
378
- emitted: summarizeDebugText(merged.delta),
379
- buffer: summarizeDebugText(reasoningBuffer),
380
- });
381
- if (merged.delta) {
382
- yield { type: "reasoning_delta", content: merged.delta };
426
+ const mergedContent = mergeStreamingText(textBuffer, delta.content, textMergeMode);
427
+ textBuffer = mergedContent.args;
428
+ const content = mergedContent.delta;
429
+ if (content) {
430
+ const thinkMatch = content.match(/<think>([\s\S]*?)<\/think>/);
431
+ if (thinkMatch) {
432
+ if (thinkMatch[1] && !hasDedicatedReasoningChannel) {
433
+ const merged = mergeStreamingText(reasoningBuffer, thinkMatch[1], reasoningMergeMode);
434
+ reasoningBuffer = merged.args;
435
+ debugReasoningStream({
436
+ stage: "provider_emit",
437
+ providerId: options.debugProviderId,
438
+ modelId: options.debugModelId,
439
+ chunkSeq: rawChunkSeq,
440
+ source: "content_think",
441
+ mergeMode: reasoningMergeMode,
442
+ suppressed: !merged.delta,
443
+ emitted: summarizeDebugText(merged.delta),
444
+ buffer: summarizeDebugText(reasoningBuffer),
445
+ });
446
+ if (merged.delta) {
447
+ yield { type: "reasoning_delta", content: merged.delta };
448
+ }
449
+ }
450
+ const remaining = content.replace(/<think>[\s\S]*?<\/think>/, "");
451
+ const cleaned = textFilter.push(remaining);
452
+ if (cleaned) {
453
+ yield { type: "text", content: cleaned };
383
454
  }
384
455
  }
385
- const remaining = delta.content.replace(/<think>[\s\S]*?<\/think>/, "");
386
- const cleaned = textFilter.push(remaining);
387
- if (cleaned) {
388
- yield { type: "text", content: cleaned };
389
- }
390
- }
391
- else {
392
- const cleaned = textFilter.push(delta.content);
393
- if (cleaned) {
394
- yield { type: "text", content: cleaned };
456
+ else {
457
+ const cleaned = textFilter.push(content);
458
+ if (cleaned) {
459
+ yield { type: "text", content: cleaned };
460
+ }
395
461
  }
396
462
  }
397
463
  }
@@ -435,6 +501,19 @@ export async function* translateOpenAIStream(stream, options = {}) {
435
501
  }
436
502
  yield* flushToolCalls();
437
503
  }
504
+ function extractReasoningDetailsText(value) {
505
+ if (!value)
506
+ return undefined;
507
+ const details = Array.isArray(value) ? value : [value];
508
+ const parts = details.flatMap((item) => {
509
+ if (!item || typeof item !== "object")
510
+ return [];
511
+ const record = item;
512
+ const text = record.text ?? record.thinking ?? record.content;
513
+ return typeof text === "string" ? [text] : [];
514
+ });
515
+ return parts.length > 0 ? parts.join("") : undefined;
516
+ }
438
517
  function mergeToolArgumentDelta(current, incoming, mode) {
439
518
  if (!current) {
440
519
  debugToolArgs({ stage: "merge", branch: "empty-current", current, incoming, args: incoming, delta: incoming });
@@ -1,4 +1,4 @@
1
- import { sanitizeInternalReminderBlocks } from "./agent/internal-reminder-sanitizer.js";
1
+ import { sanitizeAssistantProviderMetadata, sanitizeInternalReminderBlocks } from "./agent/internal-reminder-sanitizer.js";
2
2
  export class SessionLog {
3
3
  entries = [];
4
4
  load(lines) {
@@ -128,6 +128,11 @@ export class SessionLog {
128
128
  messages.push({
129
129
  ...entry.message,
130
130
  role: "assistant",
131
+ content: sanitizeInternalReminderBlocks(entry.message.content),
132
+ reasoning: entry.message.reasoning !== undefined
133
+ ? sanitizeInternalReminderBlocks(entry.message.reasoning)
134
+ : undefined,
135
+ providerMetadata: sanitizeAssistantProviderMetadata(cloneProviderMetadata(entry.message.providerMetadata)),
131
136
  });
132
137
  break;
133
138
  case "tool_call": {
@@ -190,7 +195,7 @@ function normalizeMessageToEntries(message, id, timestamp) {
190
195
  type: "assistant_message",
191
196
  message: {
192
197
  role: "assistant",
193
- content: message.content,
198
+ content: sanitizeInternalReminderBlocks(message.content),
194
199
  reasoning: message.reasoning !== undefined
195
200
  ? sanitizeInternalReminderBlocks(message.reasoning)
196
201
  : undefined,
@@ -199,6 +204,7 @@ function normalizeMessageToEntries(message, id, timestamp) {
199
204
  modelId: message.modelId,
200
205
  usage: message.usage,
201
206
  error: message.error,
207
+ providerMetadata: sanitizeAssistantProviderMetadata(cloneProviderMetadata(message.providerMetadata)),
202
208
  },
203
209
  timestamp,
204
210
  };
@@ -250,6 +256,7 @@ function cloneMessage(message) {
250
256
  return {
251
257
  ...message,
252
258
  toolCalls: message.toolCalls?.map((toolCall) => ({ ...toolCall })),
259
+ providerMetadata: cloneProviderMetadata(message.providerMetadata),
253
260
  };
254
261
  }
255
262
  if (message.role === "user" && Array.isArray(message.content)) {
@@ -263,6 +270,11 @@ function cloneMessage(message) {
263
270
  }
264
271
  return { ...message };
265
272
  }
273
+ function cloneProviderMetadata(metadata) {
274
+ if (metadata === undefined)
275
+ return undefined;
276
+ return JSON.parse(JSON.stringify(metadata));
277
+ }
266
278
  function pruneIncompleteTail(messages) {
267
279
  let currentTurnStart = -1;
268
280
  let hasCompletedAssistant = false;
@@ -4,7 +4,7 @@ import { formatDiagnostics } from "../lsp/index.js";
4
4
  import { normalizeNameForMCP } from "../mcp/name.js";
5
5
  import { parseRule } from "../permissions/rule.js";
6
6
  import { encodeModel, decodeModel, displayModel, BUILTIN_PROVIDERS, isUserVisibleProvider } from "../provider-registry.js";
7
- import { getAvailableThinkingLevels, normalizeThinkingLevel } from "../provider-transform.js";
7
+ import { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingLevel } from "../provider-transform.js";
8
8
  import { buildSystemPrompt } from "../system-prompt.js";
9
9
  import { isThinkingLevel } from "../variant/thinking-level.js";
10
10
  import { collectUsageStatsBundle, formatStatsText } from "../stats/usage.js";
@@ -131,7 +131,9 @@ function parseModelArgs(args) {
131
131
  }
132
132
  function displaySelectedModel(model, thinkingLevel) {
133
133
  const label = displayModel(model);
134
- return thinkingLevel === "off" ? label : `${label} (${thinkingLevel})`;
134
+ const { providerId, modelId } = decodeModel(model);
135
+ const defaultLevel = providerId ? getDefaultThinkingLevel(providerId, modelId) : "off";
136
+ return thinkingLevel === "off" || thinkingLevel === defaultLevel ? label : `${label} (${thinkingLevel})`;
135
137
  }
136
138
  function parseMemoryScopeArgs(args) {
137
139
  const tokens = args.trim().split(/\s+/).filter(Boolean);
package/dist/types.d.ts CHANGED
@@ -14,6 +14,14 @@ export interface ImageContent {
14
14
  export type ContentPart = TextContent | ImageContent;
15
15
  export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
16
16
  export type ReasoningEffort = ThinkingLevel;
17
+ export type ProviderRawContentBlock = Record<string, unknown> & {
18
+ type: string;
19
+ };
20
+ export interface AssistantProviderMetadata {
21
+ anthropic?: {
22
+ contentBlocks?: ProviderRawContentBlock[];
23
+ };
24
+ }
17
25
  export interface UserMessage {
18
26
  role: "user";
19
27
  content: string | ContentPart[];
@@ -23,6 +31,7 @@ export interface AssistantMessage {
23
31
  content: string;
24
32
  reasoning?: string;
25
33
  toolCalls?: ToolCall[];
34
+ providerMetadata?: AssistantProviderMetadata;
26
35
  /** Model metadata captured for local usage statistics. */
27
36
  model?: string;
28
37
  providerId?: string;
@@ -239,6 +248,10 @@ export type StreamChunk = {
239
248
  } | {
240
249
  type: "reasoning_delta";
241
250
  content: string;
251
+ } | {
252
+ type: "provider_content_block";
253
+ provider: "anthropic";
254
+ block: ProviderRawContentBlock;
242
255
  } | {
243
256
  type: "tool_call";
244
257
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bubblebrain-ai/bubble",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "A terminal coding agent",
5
5
  "type": "module",
6
6
  "engines": {