@bubblebrain-ai/bubble 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/agent/discovery-barrier.d.ts +21 -0
- package/dist/agent/discovery-barrier.js +173 -0
- package/dist/agent/internal-reminder-sanitizer.d.ts +9 -0
- package/dist/agent/internal-reminder-sanitizer.js +198 -0
- package/dist/agent/task-classifier.js +23 -5
- package/dist/agent.js +215 -30
- package/dist/context/budget.js +15 -0
- package/dist/context/projector.js +4 -3
- package/dist/debug-trace.js +14 -0
- package/dist/feishu/serve.js +1 -0
- package/dist/main.js +2 -0
- package/dist/model-catalog.d.ts +3 -0
- package/dist/model-catalog.js +44 -0
- package/dist/model-config.d.ts +3 -0
- package/dist/model-config.js +3 -0
- package/dist/model-pricing.d.ts +3 -2
- package/dist/model-pricing.js +8 -0
- package/dist/network/chatgpt-transport.d.ts +16 -0
- package/dist/network/chatgpt-transport.js +240 -0
- package/dist/oauth/openai-codex.d.ts +7 -2
- package/dist/oauth/openai-codex.js +7 -4
- package/dist/orchestrator/default-hooks.js +13 -2
- package/dist/orchestrator/hooks.d.ts +2 -0
- package/dist/prompt/compose.js +1 -1
- package/dist/prompt/reminders.js +3 -3
- package/dist/prompt/runtime.js +1 -0
- package/dist/provider-anthropic.d.ts +77 -0
- package/dist/provider-anthropic.js +544 -0
- package/dist/provider-openai-codex.d.ts +3 -0
- package/dist/provider-openai-codex.js +11 -2
- package/dist/provider-registry.d.ts +2 -0
- package/dist/provider-registry.js +29 -3
- package/dist/provider-transform.d.ts +1 -1
- package/dist/provider-transform.js +23 -0
- package/dist/provider.d.ts +4 -1
- package/dist/provider.js +119 -40
- package/dist/reasoning-debug.js +4 -1
- package/dist/session-log.js +17 -2
- package/dist/slash-commands/commands.js +4 -2
- package/dist/stats/usage.d.ts +4 -0
- package/dist/stats/usage.js +48 -11
- package/dist/tools/glob.js +3 -0
- package/dist/tools/grep.js +7 -0
- package/dist/tui/run.js +22 -12
- package/dist/tui-ink/app.js +3 -0
- package/dist/tui-ink/message-list.js +6 -3
- package/dist/tui-opentui/app.js +3 -0
- package/dist/tui-opentui/message-list.js +6 -3
- package/dist/types.d.ts +14 -1
- package/package.json +2 -1
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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;
|
package/dist/context/budget.js
CHANGED
|
@@ -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
|
|
170
|
+
return formatInternalReminderBlock(message.kind, message.content);
|
|
170
171
|
case "runtime-context":
|
|
171
172
|
default:
|
|
172
|
-
return
|
|
173
|
+
return formatInternalContextBlock(message.kind, message.content);
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
176
|
function formatRuntimeSystemMessage(message) {
|
|
176
|
-
return
|
|
177
|
+
return formatInternalContextBlock("runtime-system", message.content);
|
|
177
178
|
}
|
|
178
179
|
function cloneMessage(message) {
|
|
179
180
|
if (message.role === "assistant") {
|
package/dist/debug-trace.js
CHANGED
|
@@ -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),
|
package/dist/feishu/serve.js
CHANGED
|
@@ -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) => {
|
package/dist/model-catalog.d.ts
CHANGED
|
@@ -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 {
|
package/dist/model-catalog.js
CHANGED
|
@@ -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 },
|
package/dist/model-config.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/model-config.js
CHANGED
package/dist/model-pricing.d.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
18
|
+
currency: PricingCurrency;
|
|
18
19
|
cost: number;
|
|
19
20
|
estimated: boolean;
|
|
20
21
|
}
|
package/dist/model-pricing.js
CHANGED
|
@@ -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 {};
|