@bubblebrain-ai/bubble 0.0.54 → 0.0.56
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 +43 -0
- package/dist/agent/input-controller.d.ts +3 -0
- package/dist/agent/input-controller.js +13 -0
- package/dist/agent.js +7 -1
- package/dist/context/budget.js +7 -2
- package/dist/model-catalog.d.ts +1 -1
- package/dist/model-catalog.js +12 -2
- package/dist/model-pricing.d.ts +1 -5
- package/dist/model-pricing.js +0 -0
- package/dist/provider-ai-sdk.js +5 -0
- package/dist/provider-ark-responses.d.ts +12 -2
- package/dist/provider-ark-responses.js +68 -13
- package/dist/provider-registry.js +34 -10
- package/dist/provider.js +7 -1
- package/dist/sdk/index.d.ts +97 -6
- package/dist/sdk/index.js +445 -34
- package/dist/sdk/replay-event-log.d.ts +19 -0
- package/dist/sdk/replay-event-log.js +72 -0
- package/dist/sdk/session-turn-coordinator.d.ts +45 -0
- package/dist/sdk/session-turn-coordinator.js +227 -0
- package/dist/tui/app.js +5 -3
- package/dist/tui/controller/controller.js +12 -6
- package/dist/tui/fullscreen.js +2 -1
- package/dist/tui/model/image-paste.js +9 -10
- package/dist/types.d.ts +14 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,6 +67,49 @@ Resume your last conversation:
|
|
|
67
67
|
bubble --resume
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
## Embed with the SDK
|
|
71
|
+
|
|
72
|
+
The npm package exports `BubbleSdk` from both `@bubblebrain-ai/bubble` and
|
|
73
|
+
`@bubblebrain-ai/bubble/sdk`. Turns are serialized FIFO within one session and
|
|
74
|
+
remain parallel across different sessions.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { BubbleSdk } from "@bubblebrain-ai/bubble/sdk";
|
|
78
|
+
|
|
79
|
+
const sdk = new BubbleSdk({ defaultCwd: process.cwd() });
|
|
80
|
+
const session = sdk.createSession();
|
|
81
|
+
const events = sdk.runTurn(session.id, { prompt: "Inspect this project" });
|
|
82
|
+
|
|
83
|
+
for await (const event of events) {
|
|
84
|
+
if (event.type === "text_delta") process.stdout.write(event.content);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const queued = sdk.enqueueTurn(session.id, { prompt: "Run the tests next" });
|
|
88
|
+
void (async () => {
|
|
89
|
+
for await (const event of queued) {
|
|
90
|
+
// Render the queued turn's events.
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
While a turn owns the session slot, `sdk.steer(session.id, text)` adds text at
|
|
96
|
+
the next model-call boundary and returns a receipt: `steered` (injected into
|
|
97
|
+
the active turn), `queued` (the session is idle or the active turn can no
|
|
98
|
+
longer accept input, so the text becomes the next FIFO turn automatically),
|
|
99
|
+
or `rejected`. Every accepted receipt includes an `outcome` promise that
|
|
100
|
+
settles exactly once to `input_applied`, `input_queued`, or `input_rejected` —
|
|
101
|
+
even if the host drops the event iterator early.
|
|
102
|
+
|
|
103
|
+
Turns are driven by the SDK itself, not by iterator consumption: a returned
|
|
104
|
+
iterator is a replay subscription, so dropping it never stalls or cancels the
|
|
105
|
+
turn. `openSession(sessionId)` returns a durable handle whose `events` iterable
|
|
106
|
+
replays the whole session log from sequence 1 and `eventsFrom(n)` resumes after
|
|
107
|
+
the last sequence the host durably processed. `stop(sessionId)` is
|
|
108
|
+
Claude-style: it interrupts the active turn and keeps the queue; pass
|
|
109
|
+
`{ cancelQueued: true }` to clear both. `clearQueue(sessionId)` cancels only
|
|
110
|
+
queued turns, and `deleteSession(sessionId)` awaits teardown before the JSONL
|
|
111
|
+
file is removed.
|
|
112
|
+
|
|
70
113
|
## Model providers
|
|
71
114
|
|
|
72
115
|
Bubble ships with a catalog of built-in providers. Configure them inside the app — no environment variables required.
|
|
@@ -3,9 +3,12 @@ export declare class AgentRunInputQueue implements AgentInputController {
|
|
|
3
3
|
private readonly idPrefix;
|
|
4
4
|
private pending;
|
|
5
5
|
private nextInputId;
|
|
6
|
+
private accepting;
|
|
6
7
|
constructor(idPrefix?: string);
|
|
7
8
|
enqueue(content: string): AgentRunInput;
|
|
9
|
+
tryEnqueue(content: string): AgentRunInput | undefined;
|
|
8
10
|
drainPendingInputs(): AgentRunInput[];
|
|
9
11
|
pendingInputCount(): number;
|
|
10
12
|
clear(): AgentRunInput[];
|
|
13
|
+
closePendingInputs(): AgentRunInput[];
|
|
11
14
|
}
|
|
@@ -2,10 +2,19 @@ export class AgentRunInputQueue {
|
|
|
2
2
|
idPrefix;
|
|
3
3
|
pending = [];
|
|
4
4
|
nextInputId = 0;
|
|
5
|
+
accepting = true;
|
|
5
6
|
constructor(idPrefix = "input") {
|
|
6
7
|
this.idPrefix = idPrefix;
|
|
7
8
|
}
|
|
8
9
|
enqueue(content) {
|
|
10
|
+
const input = this.tryEnqueue(content);
|
|
11
|
+
if (!input)
|
|
12
|
+
throw new Error("Agent input queue is closed");
|
|
13
|
+
return input;
|
|
14
|
+
}
|
|
15
|
+
tryEnqueue(content) {
|
|
16
|
+
if (!this.accepting)
|
|
17
|
+
return undefined;
|
|
9
18
|
const input = {
|
|
10
19
|
id: `${this.idPrefix}-${++this.nextInputId}`,
|
|
11
20
|
content,
|
|
@@ -27,4 +36,8 @@ export class AgentRunInputQueue {
|
|
|
27
36
|
clear() {
|
|
28
37
|
return this.drainPendingInputs();
|
|
29
38
|
}
|
|
39
|
+
closePendingInputs() {
|
|
40
|
+
this.accepting = false;
|
|
41
|
+
return this.drainPendingInputs();
|
|
42
|
+
}
|
|
30
43
|
}
|
package/dist/agent.js
CHANGED
|
@@ -644,7 +644,11 @@ export class Agent {
|
|
|
644
644
|
return events;
|
|
645
645
|
};
|
|
646
646
|
const rejectPendingInputs = async (reason) => {
|
|
647
|
-
|
|
647
|
+
// Seal before the first await. A host can submit in reaction to the final
|
|
648
|
+
// turn boundary; accepting after this drain would orphan that input.
|
|
649
|
+
const pendingInputs = inputController?.closePendingInputs?.()
|
|
650
|
+
?? inputController?.drainPendingInputs()
|
|
651
|
+
?? [];
|
|
648
652
|
if (pendingInputs.length === 0)
|
|
649
653
|
return [];
|
|
650
654
|
const events = [];
|
|
@@ -697,6 +701,8 @@ export class Agent {
|
|
|
697
701
|
yield emit({ type: "turn_start" });
|
|
698
702
|
yield emit({ type: "text_delta", content: message });
|
|
699
703
|
yield emit({ type: "turn_end", willContinue: false });
|
|
704
|
+
for (const event of await rejectPendingInputs("no_continuation"))
|
|
705
|
+
yield emit(event);
|
|
700
706
|
yield emit({ type: "agent_end" });
|
|
701
707
|
return;
|
|
702
708
|
}
|
package/dist/context/budget.js
CHANGED
|
@@ -36,8 +36,11 @@ export function estimateMessageTokens(message, providerId) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
function estimateProviderMetadataOverhead(metadata, providerId) {
|
|
39
|
-
const blocks =
|
|
40
|
-
|
|
39
|
+
const blocks = [
|
|
40
|
+
...(metadata?.anthropic?.contentBlocks ?? []),
|
|
41
|
+
...(metadata?.openai?.contentBlocks ?? []),
|
|
42
|
+
];
|
|
43
|
+
if (blocks.length === 0)
|
|
41
44
|
return 0;
|
|
42
45
|
const estimate = (text) => estimateTextTokens(text, providerId);
|
|
43
46
|
return blocks.reduce((sum, block) => {
|
|
@@ -46,6 +49,8 @@ function estimateProviderMetadataOverhead(metadata, providerId) {
|
|
|
46
49
|
overhead += estimate(block.signature);
|
|
47
50
|
if (block.type === "redacted_thinking" && typeof block.data === "string")
|
|
48
51
|
overhead += estimate(block.data);
|
|
52
|
+
if (block.type === "reasoning" && typeof block.encrypted_content === "string")
|
|
53
|
+
overhead += estimate(block.encrypted_content);
|
|
49
54
|
return sum + overhead;
|
|
50
55
|
}, 0);
|
|
51
56
|
}
|
package/dist/model-catalog.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReasoningEffort } from "./types.js";
|
|
2
|
-
export type ProviderProtocol = "openai-chat" | "anthropic-messages" | "ark-responses" | "ai-sdk";
|
|
2
|
+
export type ProviderProtocol = "openai-chat" | "openai-responses" | "anthropic-messages" | "ark-responses" | "ai-sdk";
|
|
3
3
|
export interface BuiltinProviderDefinition {
|
|
4
4
|
id: string;
|
|
5
5
|
name: string;
|
package/dist/model-catalog.js
CHANGED
|
@@ -3,6 +3,7 @@ export const BUILTIN_PROVIDERS = [
|
|
|
3
3
|
{ id: "openrouter", name: "OpenRouter", baseURL: "https://openrouter.ai/api/v1" },
|
|
4
4
|
{ id: "openai", name: "OpenAI", baseURL: "https://api.openai.com/v1", supportsOAuth: true },
|
|
5
5
|
{ id: "openai-codex", name: "OpenAI Codex (ChatGPT)", baseURL: "https://chatgpt.com/backend-api" },
|
|
6
|
+
{ id: "opencode-zen", name: "OpenCode Zen", baseURL: "https://opencode.ai/zen/v1", protocol: "openai-responses" },
|
|
6
7
|
// Grok subscription models via the CLI chat proxy. OAuth-only: the proxy
|
|
7
8
|
// accepts xAI session bearers (from /login grok), not api.x.ai API keys.
|
|
8
9
|
{ id: "grok", name: "Grok Subscription", baseURL: "https://cli-chat-proxy.grok.com/v1", supportsOAuth: true },
|
|
@@ -39,6 +40,7 @@ const GPT51_LEVELS = ["off", "low", "medium", "high"];
|
|
|
39
40
|
const GPT51_CODEX_MAX_LEVELS = ["off", "low", "medium", "high", "xhigh"];
|
|
40
41
|
const GPT51_CODEX_MINI_LEVELS = ["off", "medium", "high"];
|
|
41
42
|
const OPENAI_CHAT_LEVELS = ["off"];
|
|
43
|
+
const MUSE_SPARK_LEVELS = ["minimal", "low", "medium", "high", "xhigh"];
|
|
42
44
|
// Ox Alpha declares mandatory reasoning with exactly low/high/max through
|
|
43
45
|
// OpenRouter's live model metadata. The endpoint rejects disabled reasoning.
|
|
44
46
|
const OPENROUTER_OX_REASONING_LEVELS = ["low", "high", "max"];
|
|
@@ -87,6 +89,9 @@ const ANTHROPIC_CHAT_LEVELS = ["off"];
|
|
|
87
89
|
const GROK_45_LEVELS = ["low", "medium", "high"];
|
|
88
90
|
const GEMINI_3_LEVELS = ["low", "medium", "high"];
|
|
89
91
|
const GEMINI_3_FLASH_LEVELS = ["minimal", "low", "medium", "high"];
|
|
92
|
+
// Verified against the native API on 2026-09-02: Gemini 3.7/3.8 Flash reject
|
|
93
|
+
// thinkingLevel=minimal but accept thinkingBudget=0 plus low/medium/high.
|
|
94
|
+
const GEMINI_37_38_FLASH_LEVELS = ["off", "low", "medium", "high"];
|
|
90
95
|
const GEMINI_25_PRO_LEVELS = ["low", "medium", "high"];
|
|
91
96
|
const GEMINI_25_FLASH_LEVELS = ["off", "low", "medium", "high"];
|
|
92
97
|
export const BUILTIN_MODELS = [
|
|
@@ -114,6 +119,9 @@ export const BUILTIN_MODELS = [
|
|
|
114
119
|
{ id: "o1-preview", name: "o1-preview", providerId: "openai", tier: "strong", reasoningLevels: ["off", "low", "medium", "high"], contextWindow: 128000 },
|
|
115
120
|
{ id: "o1-mini", name: "o1-mini", providerId: "openai", tier: "fast", reasoningLevels: ["off", "low", "medium", "high"], contextWindow: 128000 },
|
|
116
121
|
{ id: "gpt-4-turbo", name: "gpt-4-turbo", providerId: "openai", reasoningLevels: OPENAI_CHAT_LEVELS, contextWindow: 128000 },
|
|
122
|
+
{ id: "muse-spark-1.3-contributor-free", name: "Muse Spark 1.3 Free", providerId: "opencode-zen", reasoningLevels: MUSE_SPARK_LEVELS, defaultReasoningLevel: "high", contextWindow: 1048576 },
|
|
123
|
+
{ id: "muse-spark-1.2", name: "Muse Spark 1.2", providerId: "opencode-zen", reasoningLevels: MUSE_SPARK_LEVELS, defaultReasoningLevel: "high", contextWindow: 1048576 },
|
|
124
|
+
{ id: "muse-spark-1.2-contributor-free", name: "Muse Spark 1.2 Free", providerId: "opencode-zen", reasoningLevels: MUSE_SPARK_LEVELS, defaultReasoningLevel: "high", contextWindow: 1048576 },
|
|
117
125
|
{ id: "claude-fable-5", name: "Claude Fable 5", providerId: "anthropic", tier: "strong", reasoningLevels: ANTHROPIC_FABLE_EFFORT_LEVELS, defaultReasoningLevel: "high", contextWindow: 1000000 },
|
|
118
126
|
{ id: "claude-opus-4-8", name: "Claude Opus 4.8", providerId: "anthropic", tier: "strong", reasoningLevels: ANTHROPIC_OPUS_EFFORT_LEVELS, defaultReasoningLevel: "high", contextWindow: 1000000 },
|
|
119
127
|
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", providerId: "anthropic", tier: "balanced", reasoningLevels: ANTHROPIC_SONNET_EFFORT_LEVELS, defaultReasoningLevel: "high", contextWindow: 1000000 },
|
|
@@ -126,8 +134,10 @@ export const BUILTIN_MODELS = [
|
|
|
126
134
|
{ id: "deepseek-v4-pro", name: "deepseek-v4-pro", providerId: "deepseek", tier: "strong", reasoningLevels: DEEPSEEK_V4_LEVELS, defaultReasoningLevel: "high", contextWindow: 1048576 },
|
|
127
135
|
// Offline/no-key fallback only: with an API key the registry replaces this
|
|
128
136
|
// list via fetchGeminiModels (GET /v1beta/models, newest five). Gemini 3
|
|
129
|
-
//
|
|
130
|
-
//
|
|
137
|
+
// thinking support varies by release: 3.7/3.8 Flash support off/low/medium/high
|
|
138
|
+
// while older Flash releases expose minimal/low/medium/high.
|
|
139
|
+
{ id: "gemini-3.8-flash", name: "Gemini 3.8 Flash", providerId: "google", tier: "fast", reasoningLevels: GEMINI_37_38_FLASH_LEVELS, contextWindow: 1048576 },
|
|
140
|
+
{ id: "gemini-3.7-flash", name: "Gemini 3.7 Flash", providerId: "google", tier: "fast", reasoningLevels: GEMINI_37_38_FLASH_LEVELS, contextWindow: 1048576 },
|
|
131
141
|
{ id: "gemini-3.5-flash", name: "Gemini 3.5 Flash", providerId: "google", tier: "fast", reasoningLevels: GEMINI_3_FLASH_LEVELS, contextWindow: 1048576 },
|
|
132
142
|
{ id: "gemini-3.1-pro-preview", name: "Gemini 3.1 Pro", providerId: "google", tier: "strong", reasoningLevels: GEMINI_3_LEVELS, defaultReasoningLevel: "high", contextWindow: 1048576 },
|
|
133
143
|
{ id: "gemini-3-flash-preview", name: "Gemini 3 Flash", providerId: "google", tier: "fast", reasoningLevels: GEMINI_3_FLASH_LEVELS, contextWindow: 1048576 },
|
package/dist/model-pricing.d.ts
CHANGED
|
@@ -35,11 +35,7 @@ export interface ModelPricing extends ModelPricingRate {
|
|
|
35
35
|
/** Rate used before `effectiveFrom` (for historical session statistics). */
|
|
36
36
|
prior?: ModelPricingRate;
|
|
37
37
|
effectiveUntil?: string;
|
|
38
|
-
original?:
|
|
39
|
-
inputCacheHitPerMillion: number;
|
|
40
|
-
inputCacheMissPerMillion: number;
|
|
41
|
-
outputPerMillion: number;
|
|
42
|
-
};
|
|
38
|
+
original?: ModelPricingRate;
|
|
43
39
|
}
|
|
44
40
|
/**
|
|
45
41
|
* Hand-maintained corrections layered over the generated models.dev snapshot.
|
package/dist/model-pricing.js
CHANGED
|
Binary file
|
package/dist/provider-ai-sdk.js
CHANGED
|
@@ -509,6 +509,11 @@ export function selectLatestGeminiModels(entries, limit = 5) {
|
|
|
509
509
|
export function geminiReasoningLevels(modelId) {
|
|
510
510
|
const version = Number.parseFloat(/^gemini-(\d+(?:\.\d+)?)/.exec(modelId)?.[1] ?? "0");
|
|
511
511
|
const isPro = modelId.includes("-pro");
|
|
512
|
+
// Live probes show 3.7 and 3.8 Flash share this newer ladder. Keep the
|
|
513
|
+
// exception bounded to verified releases instead of guessing for 3.9+.
|
|
514
|
+
if (!isPro && (version === 3.7 || version === 3.8)) {
|
|
515
|
+
return ["off", "low", "medium", "high"];
|
|
516
|
+
}
|
|
512
517
|
if (version >= 3) {
|
|
513
518
|
return isPro ? ["low", "medium", "high"] : ["minimal", "low", "medium", "high"];
|
|
514
519
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Provider, ProviderMessage, StreamChunk, ThinkingLevel, TokenUsage, ToolChoiceMode, ToolDefinition } from "./types.js";
|
|
2
|
-
export interface
|
|
2
|
+
export interface ResponsesProviderOptions {
|
|
3
3
|
providerId?: string;
|
|
4
4
|
apiKey: string;
|
|
5
5
|
baseURL: string;
|
|
@@ -7,7 +7,9 @@ export interface ArkResponsesProviderOptions {
|
|
|
7
7
|
/** User-configured extra headers, merged last (client-identity gates). */
|
|
8
8
|
headers?: Record<string, string>;
|
|
9
9
|
}
|
|
10
|
+
export type ArkResponsesProviderOptions = ResponsesProviderOptions;
|
|
10
11
|
export declare function createArkResponsesProvider(options: ArkResponsesProviderOptions): Provider;
|
|
12
|
+
export declare function createOpenAIResponsesProvider(options: ResponsesProviderOptions): Provider;
|
|
11
13
|
export declare function buildArkResponsesBody(messages: ProviderMessage[], options: {
|
|
12
14
|
model: string;
|
|
13
15
|
tools?: ToolDefinition[];
|
|
@@ -15,5 +17,13 @@ export declare function buildArkResponsesBody(messages: ProviderMessage[], optio
|
|
|
15
17
|
thinkingLevel?: ThinkingLevel;
|
|
16
18
|
stream: boolean;
|
|
17
19
|
}): Record<string, unknown>;
|
|
18
|
-
export declare function
|
|
20
|
+
export declare function buildOpenAIResponsesBody(messages: ProviderMessage[], options: {
|
|
21
|
+
model: string;
|
|
22
|
+
tools?: ToolDefinition[];
|
|
23
|
+
toolChoice?: ToolChoiceMode;
|
|
24
|
+
temperature?: number;
|
|
25
|
+
thinkingLevel?: ThinkingLevel;
|
|
26
|
+
stream: boolean;
|
|
27
|
+
}): Record<string, unknown>;
|
|
28
|
+
export declare function translateArkResponsesStream(response: Response, captureOpenAIReasoning?: boolean): AsyncIterable<StreamChunk>;
|
|
19
29
|
export declare function normalizeArkResponsesUsage(usage: any): TokenUsage;
|
|
@@ -1,33 +1,43 @@
|
|
|
1
1
|
import { createProviderProtocolArtifactFilter } from "./provider-artifacts.js";
|
|
2
2
|
import { RateLimitError } from "./network/errors.js";
|
|
3
3
|
import { computeRetryDelayMs, getProviderMaxRetries, isRetryableHttpStatus, retryAfterMsFromResponse, sleepBeforeRetry, } from "./network/retry.js";
|
|
4
|
+
import { providerFetch } from "./network/provider-transport.js";
|
|
4
5
|
const DEFAULT_ARK_MODEL = "doubao-seed-2-1-pro-260628";
|
|
6
|
+
const DEFAULT_MUSE_MODEL = "muse-spark-1.3-contributor-free";
|
|
5
7
|
export function createArkResponsesProvider(options) {
|
|
8
|
+
return createResponsesProvider(options, "ark", DEFAULT_ARK_MODEL);
|
|
9
|
+
}
|
|
10
|
+
export function createOpenAIResponsesProvider(options) {
|
|
11
|
+
return createResponsesProvider(options, "openai", DEFAULT_MUSE_MODEL);
|
|
12
|
+
}
|
|
13
|
+
function createResponsesProvider(options, dialect, defaultModel) {
|
|
6
14
|
async function* streamChat(messages, chatOptions) {
|
|
7
|
-
const body =
|
|
15
|
+
const body = buildResponsesBody(messages, {
|
|
8
16
|
model: chatOptions.model,
|
|
9
17
|
tools: chatOptions.tools,
|
|
10
18
|
toolChoice: chatOptions.toolChoice,
|
|
19
|
+
temperature: chatOptions.temperature,
|
|
11
20
|
thinkingLevel: chatOptions.thinkingLevel ?? options.thinkingLevel ?? "high",
|
|
12
21
|
stream: true,
|
|
13
|
-
});
|
|
22
|
+
}, dialect);
|
|
14
23
|
for (let attempt = 0;; attempt += 1) {
|
|
15
24
|
const response = await sendArkResponsesRequest(options, body, {
|
|
16
25
|
signal: chatOptions.abortSignal,
|
|
17
26
|
rateLimitPolicy: chatOptions.rateLimitPolicy,
|
|
18
27
|
attempt,
|
|
19
28
|
});
|
|
20
|
-
yield* translateArkResponsesStream(response);
|
|
29
|
+
yield* translateArkResponsesStream(response, dialect === "openai");
|
|
21
30
|
yield { type: "done" };
|
|
22
31
|
return;
|
|
23
32
|
}
|
|
24
33
|
}
|
|
25
34
|
async function complete(messages, chatOptions) {
|
|
26
|
-
const body =
|
|
27
|
-
model: chatOptions?.model ??
|
|
35
|
+
const body = buildResponsesBody(messages, {
|
|
36
|
+
model: chatOptions?.model ?? defaultModel,
|
|
37
|
+
temperature: chatOptions?.temperature,
|
|
28
38
|
thinkingLevel: chatOptions?.thinkingLevel ?? options.thinkingLevel ?? "high",
|
|
29
39
|
stream: false,
|
|
30
|
-
});
|
|
40
|
+
}, dialect);
|
|
31
41
|
const response = await sendArkResponsesRequest(options, body, {
|
|
32
42
|
signal: chatOptions?.abortSignal,
|
|
33
43
|
attempt: 0,
|
|
@@ -38,15 +48,34 @@ export function createArkResponsesProvider(options) {
|
|
|
38
48
|
return { streamChat, complete };
|
|
39
49
|
}
|
|
40
50
|
export function buildArkResponsesBody(messages, options) {
|
|
51
|
+
return buildResponsesBody(messages, options, "ark");
|
|
52
|
+
}
|
|
53
|
+
export function buildOpenAIResponsesBody(messages, options) {
|
|
54
|
+
return buildResponsesBody(messages, options, "openai");
|
|
55
|
+
}
|
|
56
|
+
function buildResponsesBody(messages, options, dialect) {
|
|
41
57
|
const body = {
|
|
42
58
|
model: options.model,
|
|
43
|
-
input: messages.flatMap((message) =>
|
|
59
|
+
input: messages.flatMap((message) => convertMessageToResponsesInput(message, dialect)),
|
|
44
60
|
store: false,
|
|
45
61
|
stream: options.stream,
|
|
46
|
-
thinking: {
|
|
47
|
-
type: shouldDisableArkThinking(options.thinkingLevel) ? "disabled" : "enabled",
|
|
48
|
-
},
|
|
49
62
|
};
|
|
63
|
+
if (dialect === "ark") {
|
|
64
|
+
body.thinking = {
|
|
65
|
+
type: shouldDisableArkThinking(options.thinkingLevel) ? "disabled" : "enabled",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
body.include = ["reasoning.encrypted_content"];
|
|
70
|
+
if (options.thinkingLevel && options.thinkingLevel !== "off") {
|
|
71
|
+
body.reasoning = {
|
|
72
|
+
effort: options.thinkingLevel === "ultra" ? "xhigh" : options.thinkingLevel,
|
|
73
|
+
summary: "auto",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (options.temperature !== undefined)
|
|
77
|
+
body.temperature = options.temperature;
|
|
78
|
+
}
|
|
50
79
|
if (options.tools && options.tools.length > 0) {
|
|
51
80
|
body.tools = options.tools.map((tool) => ({
|
|
52
81
|
type: "function",
|
|
@@ -63,7 +92,7 @@ export function buildArkResponsesBody(messages, options) {
|
|
|
63
92
|
function shouldDisableArkThinking(level) {
|
|
64
93
|
return level === "off" || level === "minimal";
|
|
65
94
|
}
|
|
66
|
-
function
|
|
95
|
+
function convertMessageToResponsesInput(message, dialect) {
|
|
67
96
|
if (message.role === "user") {
|
|
68
97
|
if (typeof message.content === "string") {
|
|
69
98
|
return [{
|
|
@@ -92,6 +121,17 @@ function convertMessageToArkResponsesInput(message) {
|
|
|
92
121
|
}
|
|
93
122
|
if (message.role === "assistant") {
|
|
94
123
|
const items = [];
|
|
124
|
+
if (dialect === "openai") {
|
|
125
|
+
for (const block of message.providerMetadata?.openai?.contentBlocks ?? []) {
|
|
126
|
+
if (block.type !== "reasoning" || typeof block.encrypted_content !== "string")
|
|
127
|
+
continue;
|
|
128
|
+
items.push({
|
|
129
|
+
type: "reasoning",
|
|
130
|
+
summary: Array.isArray(block.summary) ? block.summary : [],
|
|
131
|
+
encrypted_content: block.encrypted_content,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
95
135
|
if (message.content) {
|
|
96
136
|
items.push({
|
|
97
137
|
type: "message",
|
|
@@ -118,7 +158,7 @@ function convertMessageToArkResponsesInput(message) {
|
|
|
118
158
|
}];
|
|
119
159
|
}
|
|
120
160
|
async function sendArkResponsesRequest(options, body, requestOptions) {
|
|
121
|
-
const response = await
|
|
161
|
+
const response = await providerFetch(resolveArkResponsesUrl(options.baseURL), {
|
|
122
162
|
method: "POST",
|
|
123
163
|
headers: {
|
|
124
164
|
"Authorization": `Bearer ${options.apiKey}`,
|
|
@@ -128,6 +168,8 @@ async function sendArkResponsesRequest(options, body, requestOptions) {
|
|
|
128
168
|
},
|
|
129
169
|
body: JSON.stringify(body),
|
|
130
170
|
signal: requestOptions.signal,
|
|
171
|
+
}, {
|
|
172
|
+
providerName: options.providerId ?? "Responses provider",
|
|
131
173
|
});
|
|
132
174
|
if (response.ok)
|
|
133
175
|
return response;
|
|
@@ -159,7 +201,7 @@ async function sendArkResponsesRequest(options, body, requestOptions) {
|
|
|
159
201
|
function resolveArkResponsesUrl(baseURL) {
|
|
160
202
|
return `${baseURL.trim().replace(/\/+$/, "")}/responses`;
|
|
161
203
|
}
|
|
162
|
-
export async function* translateArkResponsesStream(response) {
|
|
204
|
+
export async function* translateArkResponsesStream(response, captureOpenAIReasoning = false) {
|
|
163
205
|
const toolCalls = new Map();
|
|
164
206
|
const textFilter = createProviderProtocolArtifactFilter();
|
|
165
207
|
for await (const event of parseArkResponsesSse(response)) {
|
|
@@ -253,6 +295,19 @@ export async function* translateArkResponsesStream(response) {
|
|
|
253
295
|
}
|
|
254
296
|
if (type === "response.output_item.done") {
|
|
255
297
|
const item = event.item;
|
|
298
|
+
if (captureOpenAIReasoning
|
|
299
|
+
&& item?.type === "reasoning"
|
|
300
|
+
&& typeof item.encrypted_content === "string") {
|
|
301
|
+
yield {
|
|
302
|
+
type: "provider_content_block",
|
|
303
|
+
provider: "openai",
|
|
304
|
+
block: {
|
|
305
|
+
type: "reasoning",
|
|
306
|
+
summary: Array.isArray(item.summary) ? item.summary : [],
|
|
307
|
+
encrypted_content: item.encrypted_content,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
256
311
|
if (item?.type === "function_call") {
|
|
257
312
|
const key = toolCallEventKey(event, item);
|
|
258
313
|
const entry = toolCalls.get(key);
|
|
@@ -11,7 +11,7 @@ import { getBubbleHome } from "./bubble-home.js";
|
|
|
11
11
|
import { BUILTIN_PROVIDERS as CATALOG_PROVIDERS, clearDynamicModelMetadata, getBuiltinModel, getBuiltinProvider, inferGrokModelMetadata, listBuiltinModels, replaceDynamicModelMetadata, } from "./model-catalog.js";
|
|
12
12
|
import { ModelConfig } from "./model-config.js";
|
|
13
13
|
import { AuthStorage } from "./oauth/index.js";
|
|
14
|
-
import { fetchGeminiModels } from "./provider-ai-sdk.js";
|
|
14
|
+
import { fetchGeminiModels, geminiReasoningLevels } from "./provider-ai-sdk.js";
|
|
15
15
|
import { extractChatGptAccountId, fetchOpenAICodexModelCatalog } from "./provider-openai-codex.js";
|
|
16
16
|
import { fetchGrokSubscriptionModels } from "./provider-grok.js";
|
|
17
17
|
import { refreshOpenAICodex } from "./oauth/openai-codex.js";
|
|
@@ -169,18 +169,23 @@ export class ProviderRegistry {
|
|
|
169
169
|
continue;
|
|
170
170
|
if (!entry.result || !Array.isArray(entry.result.models))
|
|
171
171
|
continue;
|
|
172
|
+
const provider = { id: entry.providerId ?? "", authType: entry.authType, protocol: entry.protocol };
|
|
173
|
+
// Capability fields may have been derived by an older Bubble build.
|
|
174
|
+
// Recompute them with the current code while retaining cached remote
|
|
175
|
+
// membership, display names and context windows.
|
|
176
|
+
const result = normalizeProviderDiscoveryMetadata(provider, entry.result);
|
|
172
177
|
this.modelDiscoveryCache.set(key, {
|
|
173
|
-
result
|
|
178
|
+
result,
|
|
174
179
|
expiresAt: entry.expiresAt,
|
|
175
180
|
identityKey: entry.identityKey ?? "unknown",
|
|
176
|
-
providerId:
|
|
181
|
+
providerId: provider.id,
|
|
177
182
|
authType: entry.authType,
|
|
178
183
|
protocol: entry.protocol,
|
|
179
184
|
});
|
|
180
185
|
// Rebuild the dynamic overlay so context window / reasoning levels
|
|
181
186
|
// resolve from the cached catalog at startup, not only on /model open.
|
|
182
187
|
if (entry.providerId) {
|
|
183
|
-
this.applyDynamicDiscoveryMetadata(
|
|
188
|
+
this.applyDynamicDiscoveryMetadata(provider, result);
|
|
184
189
|
}
|
|
185
190
|
}
|
|
186
191
|
}
|
|
@@ -513,6 +518,8 @@ export class ProviderRegistry {
|
|
|
513
518
|
if (!options.forceRefresh) {
|
|
514
519
|
const cached = this.modelDiscoveryCache.get(key);
|
|
515
520
|
if (cached && cached.expiresAt > now) {
|
|
521
|
+
const cachedResult = normalizeProviderDiscoveryMetadata(provider, cached.result);
|
|
522
|
+
cached.result = cachedResult;
|
|
516
523
|
// A disk cache can outlive the application version that wrote it. Keep
|
|
517
524
|
// the current curated catalog authoritative over stale cached metadata,
|
|
518
525
|
// then retain cached remote-only ids for non-authoritative union results.
|
|
@@ -520,12 +527,12 @@ export class ProviderRegistry {
|
|
|
520
527
|
// without waiting up to 24 hours or requiring a manual Ctrl+R refresh.
|
|
521
528
|
const local = this.localModelsForProvider(provider);
|
|
522
529
|
const localIds = new Set(local.map((model) => model.id));
|
|
523
|
-
const models =
|
|
530
|
+
const models = cachedResult.source === "static"
|
|
524
531
|
? local
|
|
525
|
-
:
|
|
526
|
-
?
|
|
527
|
-
: [...local, ...
|
|
528
|
-
const result = { ...
|
|
532
|
+
: cachedResult.authoritative
|
|
533
|
+
? cachedResult.models
|
|
534
|
+
: [...local, ...cachedResult.models.filter((model) => !localIds.has(model.id))];
|
|
535
|
+
const result = { ...cachedResult, models, source: "cache" };
|
|
529
536
|
const current = this.getConfigured().find((item) => item.id === provider.id);
|
|
530
537
|
if (!current || this.modelDiscoveryKey(current) === key) {
|
|
531
538
|
this.applyDynamicDiscoveryMetadata(provider, result);
|
|
@@ -544,7 +551,8 @@ export class ProviderRegistry {
|
|
|
544
551
|
const generation = (this.modelDiscoveryGeneration.get(provider.id) ?? 0) + 1;
|
|
545
552
|
this.modelDiscoveryGeneration.set(provider.id, generation);
|
|
546
553
|
let pending;
|
|
547
|
-
pending = this.performModelDiscovery(provider).then((
|
|
554
|
+
pending = this.performModelDiscovery(provider).then((rawResult) => {
|
|
555
|
+
const result = normalizeProviderDiscoveryMetadata(provider, rawResult);
|
|
548
556
|
if (!this.isCurrentModelDiscovery(provider, key, generation)) {
|
|
549
557
|
return {
|
|
550
558
|
models: this.localModelsForProvider(this.getConfigured().find((item) => item.id === provider.id) ?? provider),
|
|
@@ -879,6 +887,22 @@ export class ProviderRegistry {
|
|
|
879
887
|
})));
|
|
880
888
|
}
|
|
881
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* Remote Gemini discovery does not expose Bubble's reasoning ladder; that
|
|
892
|
+
* metadata is inferred locally. Never trust a persisted inference across app
|
|
893
|
+
* upgrades, or a corrected capability matrix can remain stale for the cache
|
|
894
|
+
* TTL after restart.
|
|
895
|
+
*/
|
|
896
|
+
function normalizeProviderDiscoveryMetadata(provider, result) {
|
|
897
|
+
if (provider.id !== "google" || provider.protocol !== "ai-sdk")
|
|
898
|
+
return result;
|
|
899
|
+
return {
|
|
900
|
+
...result,
|
|
901
|
+
models: result.models.map((model) => model.id.startsWith("gemini-")
|
|
902
|
+
? { ...model, reasoningLevels: geminiReasoningLevels(model.id) }
|
|
903
|
+
: model),
|
|
904
|
+
};
|
|
905
|
+
}
|
|
882
906
|
function modelDiscoveryError(error) {
|
|
883
907
|
if (error instanceof Error && error.message.trim())
|
|
884
908
|
return error.message;
|
package/dist/provider.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import OpenAI from "openai";
|
|
7
7
|
import { appendFileSync } from "node:fs";
|
|
8
8
|
import { createAnthropicMessagesProvider } from "./provider-anthropic.js";
|
|
9
|
-
import { createArkResponsesProvider } from "./provider-ark-responses.js";
|
|
9
|
+
import { createArkResponsesProvider, createOpenAIResponsesProvider } from "./provider-ark-responses.js";
|
|
10
10
|
import { createAiSdkProvider } from "./provider-ai-sdk.js";
|
|
11
11
|
import { createOpenAICodexProvider, isOpenAICodexBaseUrl } from "./provider-openai-codex.js";
|
|
12
12
|
import { buildGrokSubscriptionHeaders, createGrokSubscriptionFetch, isGrokSubscriptionBaseUrl, } from "./provider-grok.js";
|
|
@@ -89,6 +89,9 @@ export function createProviderInstance(options) {
|
|
|
89
89
|
if (protocol === "ark-responses") {
|
|
90
90
|
return createArkResponsesProvider(options);
|
|
91
91
|
}
|
|
92
|
+
if (protocol === "openai-responses") {
|
|
93
|
+
return createOpenAIResponsesProvider(options);
|
|
94
|
+
}
|
|
92
95
|
if (protocol === "ai-sdk") {
|
|
93
96
|
return createAiSdkProvider(options);
|
|
94
97
|
}
|
|
@@ -273,6 +276,9 @@ function resolveProviderProtocol(options) {
|
|
|
273
276
|
return options.protocol;
|
|
274
277
|
const providerId = (options.providerId || "").toLowerCase();
|
|
275
278
|
const baseURL = (options.baseURL || "").toLowerCase();
|
|
279
|
+
if (providerId === "opencode-zen") {
|
|
280
|
+
return "openai-responses";
|
|
281
|
+
}
|
|
276
282
|
if (providerId === "doubao"
|
|
277
283
|
&& baseURL.replace(/\/+$/, "") === "https://ark.cn-beijing.volces.com/api/v3") {
|
|
278
284
|
return "ark-responses";
|