@bubblebrain-ai/bubble 0.0.33 → 0.0.35
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/agent/abort-errors.d.ts +2 -2
- package/dist/agent/budget-ledger.d.ts +4 -45
- package/dist/agent/budget-ledger.js +5 -75
- package/dist/agent/child-runner.d.ts +1 -3
- package/dist/agent/child-runner.js +6 -37
- package/dist/agent/profiles.d.ts +0 -2
- package/dist/agent/profiles.js +0 -3
- package/dist/agent/subagent-control.d.ts +5 -12
- package/dist/agent/subagent-control.js +0 -2
- package/dist/agent/subagent-store.d.ts +1 -1
- package/dist/agent/subagent-store.js +1 -3
- package/dist/agent/workflow/control.d.ts +7 -0
- package/dist/agent/workflow/control.js +22 -1
- package/dist/agent/workflow/runtime.d.ts +15 -2
- package/dist/agent/workflow/runtime.js +112 -30
- package/dist/agent.d.ts +7 -43
- package/dist/agent.js +50 -176
- package/dist/config.d.ts +1 -3
- package/dist/config.js +6 -5
- package/dist/context/overflow.js +2 -0
- package/dist/main.js +9 -1
- package/dist/model-catalog.d.ts +1 -1
- package/dist/model-catalog.js +25 -18
- package/dist/orchestrator/default-hooks.js +7 -1
- package/dist/prompt/delegation.d.ts +1 -1
- package/dist/prompt/delegation.js +21 -7
- package/dist/prompt/task-reminders.d.ts +1 -0
- package/dist/prompt/task-reminders.js +24 -1
- package/dist/provider-ai-sdk.d.ts +56 -0
- package/dist/provider-ai-sdk.js +518 -0
- package/dist/provider-registry.js +49 -3
- package/dist/provider-transform.d.ts +1 -1
- package/dist/provider-transform.js +8 -13
- package/dist/provider.js +4 -0
- package/dist/slash-commands/commands.js +3 -7
- package/dist/slash-commands/types.d.ts +1 -1
- package/dist/tools/agent-lifecycle.d.ts +1 -9
- package/dist/tools/agent-lifecycle.js +35 -263
- package/dist/tui-ink/app.js +51 -13
- package/dist/tui-ink/approval/approval-dialog.js +1 -1
- package/dist/tui-ink/approval/diff-view.js +4 -4
- package/dist/tui-ink/approval/select.js +2 -2
- package/dist/tui-ink/feedback-dialog.js +2 -2
- package/dist/tui-ink/feishu-setup-picker.js +3 -3
- package/dist/tui-ink/input-box.d.ts +1 -6
- package/dist/tui-ink/input-box.js +14 -35
- package/dist/tui-ink/markdown.js +11 -9
- package/dist/tui-ink/message-list.d.ts +5 -1
- package/dist/tui-ink/message-list.js +15 -2
- package/dist/tui-ink/model-picker.d.ts +3 -1
- package/dist/tui-ink/model-picker.js +25 -14
- package/dist/tui-ink/plan-confirm.js +1 -1
- package/dist/tui-ink/subagent-view.d.ts +3 -1
- package/dist/tui-ink/theme.d.ts +19 -2
- package/dist/tui-ink/theme.js +25 -8
- package/dist/tui-ink/welcome.d.ts +3 -2
- package/dist/tui-ink/welcome.js +10 -6
- package/dist/types.d.ts +16 -30
- package/dist/variant/variant-resolver.d.ts +13 -0
- package/dist/variant/variant-resolver.js +26 -0
- package/package.json +9 -3
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI SDK provider backend ("ai-sdk" protocol).
|
|
3
|
+
*
|
|
4
|
+
* Consumes AI SDK provider packages at the LanguageModelV3 spec layer
|
|
5
|
+
* (`model.doStream`) rather than through the high-level `streamText` API:
|
|
6
|
+
* pre-stream HTTP errors throw natively (which our retry/rate-limit contract
|
|
7
|
+
* needs), tool-call inputs arrive as JSON strings (matching `argumentsFull`),
|
|
8
|
+
* and no step machinery or extra dependencies come along.
|
|
9
|
+
*
|
|
10
|
+
* Registered providers: google (Gemini native API). Adding another AI SDK
|
|
11
|
+
* provider is one entry in AI_SDK_PROVIDER_FACTORIES.
|
|
12
|
+
*/
|
|
13
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
14
|
+
import { APICallError } from "@ai-sdk/provider";
|
|
15
|
+
import { RateLimitError } from "./network/errors.js";
|
|
16
|
+
import { ProviderStreamInterruptedError, computeRetryDelayMs, getProviderMaxRetries, isRetryableHttpStatus, sleepBeforeRetry, } from "./network/retry.js";
|
|
17
|
+
import { createProviderFetch, isProviderTransportError } from "./network/provider-transport.js";
|
|
18
|
+
const GEMINI_DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
19
|
+
const AI_SDK_PROVIDER_FACTORIES = {
|
|
20
|
+
google: (options) => {
|
|
21
|
+
const provider = createGoogleGenerativeAI({
|
|
22
|
+
apiKey: options.apiKey,
|
|
23
|
+
...(options.baseURL ? { baseURL: normalizeBaseURL(options.baseURL) } : {}),
|
|
24
|
+
fetch: (options.fetch ?? createProviderFetch({
|
|
25
|
+
providerName: "Google Gemini",
|
|
26
|
+
verboseEnvVar: "BUBBLE_AI_SDK_FETCH_VERBOSE",
|
|
27
|
+
})),
|
|
28
|
+
});
|
|
29
|
+
return (modelId) => provider(modelId);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
export function isAiSdkProviderId(providerId) {
|
|
33
|
+
return !!providerId && providerId in AI_SDK_PROVIDER_FACTORIES;
|
|
34
|
+
}
|
|
35
|
+
export function createAiSdkProvider(options) {
|
|
36
|
+
const providerId = options.providerId || "google";
|
|
37
|
+
const factory = AI_SDK_PROVIDER_FACTORIES[providerId];
|
|
38
|
+
if (!factory) {
|
|
39
|
+
const known = Object.keys(AI_SDK_PROVIDER_FACTORIES).join(", ");
|
|
40
|
+
throw new Error(`Provider "${providerId}" is configured with protocol "ai-sdk" but no AI SDK backend is registered for it (known: ${known}).`);
|
|
41
|
+
}
|
|
42
|
+
const getModel = factory(options);
|
|
43
|
+
async function* streamChat(messages, chatOptions) {
|
|
44
|
+
const model = getModel(chatOptions.model);
|
|
45
|
+
const callOptions = buildCallOptions(messages, chatOptions, options);
|
|
46
|
+
const maxRetries = getProviderMaxRetries();
|
|
47
|
+
for (let attempt = 0;; attempt++) {
|
|
48
|
+
let stream;
|
|
49
|
+
try {
|
|
50
|
+
({ stream } = await model.doStream(callOptions));
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
// No stream established: the request is safe to classify and re-issue.
|
|
54
|
+
handlePreStreamError(error, {
|
|
55
|
+
attempt,
|
|
56
|
+
maxRetries,
|
|
57
|
+
rateLimitPolicy: chatOptions.rateLimitPolicy,
|
|
58
|
+
signal: chatOptions.abortSignal,
|
|
59
|
+
});
|
|
60
|
+
await sleepBeforeRetry(computeRetryDelayMs(attempt + 1, { retryAfterMs: retryAfterMsFromError(error) }), chatOptions.abortSignal);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const translator = new StreamTranslator();
|
|
64
|
+
let surfacedContent = false;
|
|
65
|
+
try {
|
|
66
|
+
for await (const part of stream) {
|
|
67
|
+
// The SDK surfaces post-200 failures as error parts and lets the
|
|
68
|
+
// stream "finish" normally; that would look like an empty reply to
|
|
69
|
+
// the agent loop, so convert them back into throws here.
|
|
70
|
+
if (part.type === "error") {
|
|
71
|
+
throw coerceError(part.error);
|
|
72
|
+
}
|
|
73
|
+
for (const chunk of translator.translate(part)) {
|
|
74
|
+
surfacedContent = surfacedContent || isContentChunk(chunk);
|
|
75
|
+
yield chunk;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
yield { type: "done" };
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (chatOptions.abortSignal?.aborted)
|
|
83
|
+
throw coerceError(error);
|
|
84
|
+
if (surfacedContent) {
|
|
85
|
+
// Partial content already reached the UI — only the agent loop can
|
|
86
|
+
// discard the half-built assistant message and re-issue the request.
|
|
87
|
+
throw new ProviderStreamInterruptedError(`Gemini stream interrupted: ${errorMessage(error)}`, { cause: error });
|
|
88
|
+
}
|
|
89
|
+
if (attempt >= maxRetries || !isRetryableStreamError(error))
|
|
90
|
+
throw coerceError(error);
|
|
91
|
+
await sleepBeforeRetry(computeRetryDelayMs(attempt + 1), chatOptions.abortSignal);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function complete(messages, completeOptions) {
|
|
96
|
+
const modelId = completeOptions?.model;
|
|
97
|
+
if (!modelId)
|
|
98
|
+
throw new Error("ai-sdk provider requires an explicit model for complete().");
|
|
99
|
+
const model = getModel(modelId);
|
|
100
|
+
const callOptions = buildCallOptions(messages, {
|
|
101
|
+
model: modelId,
|
|
102
|
+
temperature: completeOptions?.temperature,
|
|
103
|
+
thinkingLevel: completeOptions?.thinkingLevel ?? "off",
|
|
104
|
+
abortSignal: completeOptions?.abortSignal,
|
|
105
|
+
}, options);
|
|
106
|
+
const result = await model.doGenerate(callOptions);
|
|
107
|
+
return result.content
|
|
108
|
+
.filter((part) => part.type === "text")
|
|
109
|
+
.map((part) => part.text)
|
|
110
|
+
.join("");
|
|
111
|
+
}
|
|
112
|
+
return { streamChat, complete };
|
|
113
|
+
}
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// Request building
|
|
116
|
+
// ============================================================================
|
|
117
|
+
function buildCallOptions(messages, chatOptions, providerOptions) {
|
|
118
|
+
const prompt = convertMessages(messages);
|
|
119
|
+
const thinking = buildGoogleThinkingOptions(chatOptions.model, chatOptions.thinkingLevel ?? providerOptions.thinkingLevel);
|
|
120
|
+
return {
|
|
121
|
+
prompt,
|
|
122
|
+
...(chatOptions.temperature !== undefined ? { temperature: chatOptions.temperature } : {}),
|
|
123
|
+
...(chatOptions.abortSignal ? { abortSignal: chatOptions.abortSignal } : {}),
|
|
124
|
+
...(chatOptions.tools?.length
|
|
125
|
+
? {
|
|
126
|
+
tools: chatOptions.tools.map((tool) => ({
|
|
127
|
+
type: "function",
|
|
128
|
+
name: tool.name,
|
|
129
|
+
description: tool.description,
|
|
130
|
+
inputSchema: tool.parameters,
|
|
131
|
+
})),
|
|
132
|
+
toolChoice: { type: chatOptions.toolChoice === "none" ? "none" : "auto" },
|
|
133
|
+
}
|
|
134
|
+
: {}),
|
|
135
|
+
...(thinking ? { providerOptions: { google: thinking } } : {}),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Gemini 3 models take a graded thinking_level; 2.5-era models take a token
|
|
140
|
+
* budget. "off" (budget 0) is only offered in the catalog for models that
|
|
141
|
+
* accept it (2.5 Flash); xhigh/max clamp to high.
|
|
142
|
+
*/
|
|
143
|
+
function buildGoogleThinkingOptions(modelId, level) {
|
|
144
|
+
if (!level)
|
|
145
|
+
return undefined;
|
|
146
|
+
if (level === "off")
|
|
147
|
+
return { thinkingConfig: { thinkingBudget: 0 } };
|
|
148
|
+
const clamped = level === "xhigh" || level === "max" ? "high" : level;
|
|
149
|
+
if (modelId.includes("gemini-3")) {
|
|
150
|
+
return { thinkingConfig: { thinkingLevel: clamped, includeThoughts: true } };
|
|
151
|
+
}
|
|
152
|
+
const budgets = { minimal: 512, low: 2048, medium: 8192, high: 24576 };
|
|
153
|
+
return { thinkingConfig: { thinkingBudget: budgets[clamped] ?? 8192, includeThoughts: true } };
|
|
154
|
+
}
|
|
155
|
+
function convertMessages(messages) {
|
|
156
|
+
const out = [];
|
|
157
|
+
const toolNamesById = new Map();
|
|
158
|
+
for (const message of messages) {
|
|
159
|
+
if (message.role === "system") {
|
|
160
|
+
out.push({ role: "system", content: message.content });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (message.role === "user") {
|
|
164
|
+
out.push({ role: "user", content: convertUserContent(message.content) });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (message.role === "assistant") {
|
|
168
|
+
for (const call of message.toolCalls ?? [])
|
|
169
|
+
toolNamesById.set(call.id, call.name);
|
|
170
|
+
const content = convertAssistantContent(message);
|
|
171
|
+
if (content.length > 0)
|
|
172
|
+
out.push({ role: "assistant", content });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
// tool result
|
|
176
|
+
out.push({
|
|
177
|
+
role: "tool",
|
|
178
|
+
content: [{
|
|
179
|
+
type: "tool-result",
|
|
180
|
+
toolCallId: message.toolCallId,
|
|
181
|
+
toolName: toolNamesById.get(message.toolCallId) ?? "unknown_tool",
|
|
182
|
+
output: toToolResultOutput(message.content, message.isError),
|
|
183
|
+
}],
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
function convertUserContent(content) {
|
|
189
|
+
if (typeof content === "string")
|
|
190
|
+
return [{ type: "text", text: content }];
|
|
191
|
+
const parts = [];
|
|
192
|
+
for (const part of content) {
|
|
193
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
194
|
+
parts.push({ type: "text", text: part.text });
|
|
195
|
+
}
|
|
196
|
+
else if (part.type === "image_url" && part.image_url?.url) {
|
|
197
|
+
parts.push(toImageFilePart(part.image_url.url));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return parts.length > 0 ? parts : [{ type: "text", text: "" }];
|
|
201
|
+
}
|
|
202
|
+
function toImageFilePart(url) {
|
|
203
|
+
const dataUrl = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(url);
|
|
204
|
+
if (dataUrl) {
|
|
205
|
+
return {
|
|
206
|
+
type: "file",
|
|
207
|
+
mediaType: dataUrl[1] || "image/png",
|
|
208
|
+
data: dataUrl[2] ? dataUrl[3] : decodeURIComponent(dataUrl[3]),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const extension = /\.(png|jpe?g|gif|webp)(?:[?#]|$)/i.exec(url)?.[1]?.toLowerCase();
|
|
212
|
+
const mediaType = extension === "jpg" || extension === "jpeg"
|
|
213
|
+
? "image/jpeg"
|
|
214
|
+
: extension
|
|
215
|
+
? `image/${extension}`
|
|
216
|
+
: "image/png";
|
|
217
|
+
return { type: "file", mediaType, data: new URL(url) };
|
|
218
|
+
}
|
|
219
|
+
function convertAssistantContent(message) {
|
|
220
|
+
const parts = [];
|
|
221
|
+
const blocks = message.providerMetadata?.google?.contentBlocks ?? [];
|
|
222
|
+
const toolCallSignatures = new Map();
|
|
223
|
+
// Replay captured Gemini parts: signed reasoning comes back verbatim so the
|
|
224
|
+
// thought signature round-trips; tool-call signatures re-attach by id.
|
|
225
|
+
for (const block of blocks) {
|
|
226
|
+
if (block.type === "reasoning" && typeof block.text === "string" && typeof block.thoughtSignature === "string") {
|
|
227
|
+
parts.push({
|
|
228
|
+
type: "reasoning",
|
|
229
|
+
text: block.text,
|
|
230
|
+
providerOptions: { google: { thoughtSignature: block.thoughtSignature } },
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
else if (block.type === "tool-call" && typeof block.toolCallId === "string" && typeof block.thoughtSignature === "string") {
|
|
234
|
+
toolCallSignatures.set(block.toolCallId, block.thoughtSignature);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (message.content)
|
|
238
|
+
parts.push({ type: "text", text: message.content });
|
|
239
|
+
for (const call of message.toolCalls ?? []) {
|
|
240
|
+
const signature = toolCallSignatures.get(call.id);
|
|
241
|
+
parts.push({
|
|
242
|
+
type: "tool-call",
|
|
243
|
+
toolCallId: call.id,
|
|
244
|
+
toolName: call.name,
|
|
245
|
+
input: parseToolArguments(call.arguments),
|
|
246
|
+
...(signature ? { providerOptions: { google: { thoughtSignature: signature } } } : {}),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return parts;
|
|
250
|
+
}
|
|
251
|
+
function parseToolArguments(raw) {
|
|
252
|
+
if (!raw)
|
|
253
|
+
return {};
|
|
254
|
+
try {
|
|
255
|
+
const parsed = JSON.parse(raw);
|
|
256
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return {};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function toToolResultOutput(content, isError) {
|
|
263
|
+
return isError ? { type: "error-text", value: content } : { type: "text", value: content };
|
|
264
|
+
}
|
|
265
|
+
function normalizeBaseURL(baseURL) {
|
|
266
|
+
return baseURL.trim().replace(/\/+$/, "");
|
|
267
|
+
}
|
|
268
|
+
// ============================================================================
|
|
269
|
+
// Stream translation
|
|
270
|
+
// ============================================================================
|
|
271
|
+
class StreamTranslator {
|
|
272
|
+
reasoningText = "";
|
|
273
|
+
reasoningSignature;
|
|
274
|
+
toolNamesById = new Map();
|
|
275
|
+
translate(part) {
|
|
276
|
+
switch (part.type) {
|
|
277
|
+
case "text-delta":
|
|
278
|
+
return part.delta ? [{ type: "text", content: part.delta }] : [];
|
|
279
|
+
case "reasoning-start":
|
|
280
|
+
this.reasoningText = "";
|
|
281
|
+
this.reasoningSignature = thoughtSignatureOf(part.providerMetadata);
|
|
282
|
+
return [];
|
|
283
|
+
case "reasoning-delta": {
|
|
284
|
+
this.reasoningText += part.delta;
|
|
285
|
+
this.reasoningSignature ??= thoughtSignatureOf(part.providerMetadata);
|
|
286
|
+
return part.delta ? [{ type: "reasoning_delta", content: part.delta }] : [];
|
|
287
|
+
}
|
|
288
|
+
case "reasoning-end": {
|
|
289
|
+
this.reasoningSignature ??= thoughtSignatureOf(part.providerMetadata);
|
|
290
|
+
if (!this.reasoningSignature || !this.reasoningText)
|
|
291
|
+
return [];
|
|
292
|
+
const block = {
|
|
293
|
+
type: "reasoning",
|
|
294
|
+
text: this.reasoningText,
|
|
295
|
+
thoughtSignature: this.reasoningSignature,
|
|
296
|
+
};
|
|
297
|
+
this.reasoningText = "";
|
|
298
|
+
this.reasoningSignature = undefined;
|
|
299
|
+
return [{ type: "provider_content_block", provider: "google", block }];
|
|
300
|
+
}
|
|
301
|
+
case "tool-input-start":
|
|
302
|
+
this.toolNamesById.set(part.id, part.toolName);
|
|
303
|
+
return [{ type: "tool_call", id: part.id, name: part.toolName, arguments: "", isStart: true, isEnd: false }];
|
|
304
|
+
case "tool-input-delta": {
|
|
305
|
+
if (!part.delta)
|
|
306
|
+
return [];
|
|
307
|
+
const name = this.toolNamesById.get(part.id) ?? "";
|
|
308
|
+
return [{ type: "tool_call", id: part.id, name, arguments: part.delta, isStart: false, isEnd: false }];
|
|
309
|
+
}
|
|
310
|
+
case "tool-call": {
|
|
311
|
+
const argumentsFull = typeof part.input === "string" ? part.input : JSON.stringify(part.input ?? {});
|
|
312
|
+
const chunks = [{
|
|
313
|
+
type: "tool_call",
|
|
314
|
+
id: part.toolCallId,
|
|
315
|
+
name: part.toolName,
|
|
316
|
+
arguments: "",
|
|
317
|
+
isStart: false,
|
|
318
|
+
isEnd: true,
|
|
319
|
+
argumentsFull,
|
|
320
|
+
}];
|
|
321
|
+
const signature = thoughtSignatureOf(part.providerMetadata);
|
|
322
|
+
if (signature) {
|
|
323
|
+
chunks.push({
|
|
324
|
+
type: "provider_content_block",
|
|
325
|
+
provider: "google",
|
|
326
|
+
block: { type: "tool-call", toolCallId: part.toolCallId, thoughtSignature: signature },
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return chunks;
|
|
330
|
+
}
|
|
331
|
+
case "finish": {
|
|
332
|
+
const usage = translateUsage(part.usage);
|
|
333
|
+
return usage ? [{ type: "usage", usage }] : [];
|
|
334
|
+
}
|
|
335
|
+
default:
|
|
336
|
+
// stream-start, response-metadata, text-start/end, tool-input-end,
|
|
337
|
+
// source, raw: nothing to surface.
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function thoughtSignatureOf(metadata) {
|
|
343
|
+
const signature = metadata?.google?.thoughtSignature;
|
|
344
|
+
return typeof signature === "string" && signature.length > 0 ? signature : undefined;
|
|
345
|
+
}
|
|
346
|
+
function translateUsage(usage) {
|
|
347
|
+
const inputTokens = usage?.inputTokens?.total;
|
|
348
|
+
const outputTokens = usage?.outputTokens?.total;
|
|
349
|
+
// Without a real input count the chunk would poison the agent's context
|
|
350
|
+
// budget tracking (lastInputTokens), so skip rather than report zeros.
|
|
351
|
+
if (!Number.isFinite(inputTokens))
|
|
352
|
+
return undefined;
|
|
353
|
+
const result = {
|
|
354
|
+
promptTokens: inputTokens,
|
|
355
|
+
completionTokens: Number.isFinite(outputTokens) ? outputTokens : 0,
|
|
356
|
+
};
|
|
357
|
+
const cacheRead = usage?.inputTokens?.cacheRead;
|
|
358
|
+
const noCache = usage?.inputTokens?.noCache;
|
|
359
|
+
const cacheWrite = usage?.inputTokens?.cacheWrite;
|
|
360
|
+
const reasoning = usage?.outputTokens?.reasoning;
|
|
361
|
+
if (Number.isFinite(cacheRead))
|
|
362
|
+
result.promptCacheHitTokens = cacheRead;
|
|
363
|
+
if (Number.isFinite(noCache))
|
|
364
|
+
result.promptCacheMissTokens = noCache;
|
|
365
|
+
if (Number.isFinite(cacheWrite))
|
|
366
|
+
result.cacheCreationTokens = cacheWrite;
|
|
367
|
+
if (Number.isFinite(reasoning))
|
|
368
|
+
result.reasoningTokens = reasoning;
|
|
369
|
+
if (Number.isFinite(inputTokens) && Number.isFinite(outputTokens)) {
|
|
370
|
+
result.totalTokens = inputTokens + outputTokens;
|
|
371
|
+
}
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
function isContentChunk(chunk) {
|
|
375
|
+
return chunk.type === "text" || chunk.type === "reasoning_delta" || chunk.type === "tool_call";
|
|
376
|
+
}
|
|
377
|
+
// ============================================================================
|
|
378
|
+
// Error handling
|
|
379
|
+
// ============================================================================
|
|
380
|
+
/**
|
|
381
|
+
* Classify a pre-stream error. Throws (RateLimitError / the original error)
|
|
382
|
+
* when the request must not be retried here; returns normally when the caller
|
|
383
|
+
* should back off and retry.
|
|
384
|
+
*/
|
|
385
|
+
function handlePreStreamError(error, context) {
|
|
386
|
+
if (context.signal?.aborted)
|
|
387
|
+
throw coerceError(error);
|
|
388
|
+
const status = APICallError.isInstance(error) ? error.statusCode : undefined;
|
|
389
|
+
if (status === 429) {
|
|
390
|
+
const retryAfterMs = retryAfterMsFromError(error);
|
|
391
|
+
if (context.rateLimitPolicy === "defer") {
|
|
392
|
+
// Rate-limit contract: under "defer" the transport does no 429 backoff;
|
|
393
|
+
// the subagent scheduler owns it.
|
|
394
|
+
throw new RateLimitError(`Gemini API rate limited (429): ${errorMessage(error)}`, {
|
|
395
|
+
status: 429,
|
|
396
|
+
retryAfterMs,
|
|
397
|
+
cause: error,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
if (context.attempt >= context.maxRetries) {
|
|
401
|
+
throw new RateLimitError(`Gemini API rate limited (429) after ${context.attempt + 1} attempts: ${errorMessage(error)}`, { status: 429, retryAfterMs, cause: error });
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const retryable = APICallError.isInstance(error)
|
|
406
|
+
? (error.isRetryable || (typeof status === "number" && isRetryableHttpStatus(status)))
|
|
407
|
+
: isProviderTransportError(error);
|
|
408
|
+
if (!retryable || context.attempt >= context.maxRetries)
|
|
409
|
+
throw coerceError(error);
|
|
410
|
+
}
|
|
411
|
+
function retryAfterMsFromError(error) {
|
|
412
|
+
if (!APICallError.isInstance(error))
|
|
413
|
+
return undefined;
|
|
414
|
+
const header = error.responseHeaders?.["retry-after"]?.trim();
|
|
415
|
+
if (!header)
|
|
416
|
+
return undefined;
|
|
417
|
+
const seconds = Number(header);
|
|
418
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
419
|
+
return Math.round(seconds * 1000);
|
|
420
|
+
const date = Date.parse(header);
|
|
421
|
+
if (!Number.isNaN(date))
|
|
422
|
+
return Math.max(0, date - Date.now());
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
function isRetryableStreamError(error) {
|
|
426
|
+
if (APICallError.isInstance(error)) {
|
|
427
|
+
return error.isRetryable || (typeof error.statusCode === "number" && isRetryableHttpStatus(error.statusCode));
|
|
428
|
+
}
|
|
429
|
+
return isProviderTransportError(error);
|
|
430
|
+
}
|
|
431
|
+
function coerceError(error) {
|
|
432
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
433
|
+
}
|
|
434
|
+
function errorMessage(error) {
|
|
435
|
+
return error instanceof Error ? error.message : String(error);
|
|
436
|
+
}
|
|
437
|
+
// Specialised variants that are not general text/coding models.
|
|
438
|
+
const GEMINI_MODEL_EXCLUDE = /(tts|image|audio|embedding|live|computer-use|robotics|banana|aqa|learnlm|gemma|imagen|veo)/i;
|
|
439
|
+
/**
|
|
440
|
+
* Fetch Google's model list and keep the newest general-purpose text models.
|
|
441
|
+
* Newly released Gemini versions appear without a catalog change; the static
|
|
442
|
+
* BUILTIN_MODELS entries stay as the offline/no-key fallback.
|
|
443
|
+
*/
|
|
444
|
+
export async function fetchGeminiModels(options) {
|
|
445
|
+
const baseURL = normalizeBaseURL(options.baseURL || GEMINI_DEFAULT_BASE_URL);
|
|
446
|
+
const fetchImpl = options.fetch ?? createProviderFetch({
|
|
447
|
+
providerName: "Google Gemini",
|
|
448
|
+
verboseEnvVar: "BUBBLE_AI_SDK_FETCH_VERBOSE",
|
|
449
|
+
});
|
|
450
|
+
const response = await fetchImpl(`${baseURL}/models?pageSize=1000`, {
|
|
451
|
+
headers: { "x-goog-api-key": options.apiKey },
|
|
452
|
+
});
|
|
453
|
+
if (!response.ok) {
|
|
454
|
+
throw new Error(`Gemini model list failed (${response.status}): ${await response.text().catch(() => response.statusText)}`);
|
|
455
|
+
}
|
|
456
|
+
const data = await response.json();
|
|
457
|
+
return selectLatestGeminiModels(data.models ?? [], options.limit ?? 5);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Ranking: one entry per (version, tier) family — GA ids beat dated previews —
|
|
461
|
+
* then newest version first, pro before flash before flash-lite, top N.
|
|
462
|
+
*/
|
|
463
|
+
export function selectLatestGeminiModels(entries, limit = 5) {
|
|
464
|
+
const TIER_RANK = { pro: 0, flash: 1, "flash-lite": 2 };
|
|
465
|
+
const candidates = [];
|
|
466
|
+
for (const entry of entries) {
|
|
467
|
+
const id = (entry.name ?? "").replace(/^models\//, "");
|
|
468
|
+
if (!id.startsWith("gemini-"))
|
|
469
|
+
continue;
|
|
470
|
+
if (GEMINI_MODEL_EXCLUDE.test(id))
|
|
471
|
+
continue;
|
|
472
|
+
if (entry.supportedGenerationMethods && !entry.supportedGenerationMethods.includes("generateContent"))
|
|
473
|
+
continue;
|
|
474
|
+
const match = /^gemini-(\d+(?:\.\d+)?)-(pro|flash)(-lite)?/.exec(id);
|
|
475
|
+
if (!match)
|
|
476
|
+
continue;
|
|
477
|
+
candidates.push({
|
|
478
|
+
id,
|
|
479
|
+
entry,
|
|
480
|
+
version: Number.parseFloat(match[1]),
|
|
481
|
+
tierRank: TIER_RANK[`${match[2]}${match[3] ?? ""}`] ?? 3,
|
|
482
|
+
isPreview: id.includes("preview") || id.includes("exp"),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
// One winner per family: GA over preview, then the shortest (least-suffixed) id.
|
|
486
|
+
const families = new Map();
|
|
487
|
+
for (const candidate of candidates) {
|
|
488
|
+
const key = `${candidate.version}-${candidate.tierRank}`;
|
|
489
|
+
const current = families.get(key);
|
|
490
|
+
if (!current
|
|
491
|
+
|| (current.isPreview && !candidate.isPreview)
|
|
492
|
+
|| (current.isPreview === candidate.isPreview && candidate.id.length < current.id.length)) {
|
|
493
|
+
families.set(key, candidate);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return [...families.values()]
|
|
497
|
+
.sort((a, b) => (b.version - a.version) || (a.tierRank - b.tierRank))
|
|
498
|
+
.slice(0, limit)
|
|
499
|
+
.map((candidate) => ({
|
|
500
|
+
id: candidate.id,
|
|
501
|
+
name: candidate.entry.displayName || candidate.id,
|
|
502
|
+
contextWindow: Number.isFinite(candidate.entry.inputTokenLimit) ? candidate.entry.inputTokenLimit : undefined,
|
|
503
|
+
reasoningLevels: geminiReasoningLevels(candidate.id),
|
|
504
|
+
...(candidate.tierRank === 0 ? { defaultReasoningLevel: "high" } : {}),
|
|
505
|
+
}));
|
|
506
|
+
}
|
|
507
|
+
/** Mirrors the static catalog's per-family thinking support. */
|
|
508
|
+
export function geminiReasoningLevels(modelId) {
|
|
509
|
+
const version = Number.parseFloat(/^gemini-(\d+(?:\.\d+)?)/.exec(modelId)?.[1] ?? "0");
|
|
510
|
+
const isPro = modelId.includes("-pro");
|
|
511
|
+
if (version >= 3) {
|
|
512
|
+
return isPro ? ["low", "medium", "high"] : ["minimal", "low", "medium", "high"];
|
|
513
|
+
}
|
|
514
|
+
if (version >= 2.5) {
|
|
515
|
+
return isPro ? ["low", "medium", "high"] : ["off", "low", "medium", "high"];
|
|
516
|
+
}
|
|
517
|
+
return ["off"];
|
|
518
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { BUILTIN_PROVIDERS as CATALOG_PROVIDERS, getBuiltinModel, getBuiltinProvider, listBuiltinModels, registerDynamicModelMetadata, } from "./model-catalog.js";
|
|
8
8
|
import { ModelConfig } from "./model-config.js";
|
|
9
9
|
import { AuthStorage } from "./oauth/index.js";
|
|
10
|
+
import { fetchGeminiModels } from "./provider-ai-sdk.js";
|
|
10
11
|
import { fetchOpenAICodexModels } from "./provider-openai-codex.js";
|
|
11
12
|
import { refreshOpenAICodex } from "./oauth/openai-codex.js";
|
|
12
13
|
export const BUILTIN_PROVIDERS = CATALOG_PROVIDERS;
|
|
@@ -123,7 +124,7 @@ export class ProviderRegistry {
|
|
|
123
124
|
providers = keys.map((id) => {
|
|
124
125
|
const builtin = getBuiltinProvider(id);
|
|
125
126
|
const cfg = modelsJsonProviders[id];
|
|
126
|
-
const baseURL = cfg.baseURL || builtin?.baseURL || "";
|
|
127
|
+
const baseURL = upgradeLegacyBaseURL(id, cfg.baseURL || builtin?.baseURL || "", cfg.protocol);
|
|
127
128
|
return {
|
|
128
129
|
id,
|
|
129
130
|
name: builtin?.name || id,
|
|
@@ -138,10 +139,11 @@ export class ProviderRegistry {
|
|
|
138
139
|
else {
|
|
139
140
|
// 2. Fall back to config.json providers (interactive TUI style)
|
|
140
141
|
providers = this.config.getProviders().map((provider) => {
|
|
141
|
-
const
|
|
142
|
+
const baseURL = upgradeLegacyBaseURL(provider.id, provider.baseURL, provider.protocol);
|
|
142
143
|
return {
|
|
143
144
|
...provider,
|
|
144
|
-
|
|
145
|
+
baseURL,
|
|
146
|
+
protocol: resolveConfiguredProtocol(provider.id, baseURL, provider.protocol),
|
|
145
147
|
};
|
|
146
148
|
});
|
|
147
149
|
}
|
|
@@ -247,6 +249,31 @@ export class ProviderRegistry {
|
|
|
247
249
|
// fall through to static
|
|
248
250
|
}
|
|
249
251
|
}
|
|
252
|
+
if (provider.id === "google" && provider.protocol === "ai-sdk" && provider.apiKey) {
|
|
253
|
+
try {
|
|
254
|
+
const descriptors = await fetchGeminiModels({
|
|
255
|
+
apiKey: provider.apiKey,
|
|
256
|
+
baseURL: provider.baseURL,
|
|
257
|
+
});
|
|
258
|
+
if (descriptors.length > 0) {
|
|
259
|
+
for (const d of descriptors) {
|
|
260
|
+
const catalogEntry = getBuiltinModel("google", d.id);
|
|
261
|
+
registerDynamicModelMetadata({
|
|
262
|
+
id: d.id,
|
|
263
|
+
name: d.name,
|
|
264
|
+
providerId: "google",
|
|
265
|
+
reasoningLevels: d.reasoningLevels,
|
|
266
|
+
defaultReasoningLevel: d.defaultReasoningLevel ?? catalogEntry?.defaultReasoningLevel,
|
|
267
|
+
contextWindow: d.contextWindow ?? catalogEntry?.contextWindow,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return descriptors.map((d) => ({ id: d.id, name: d.name, providerId: provider.id }));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
// fall through to static
|
|
275
|
+
}
|
|
276
|
+
}
|
|
250
277
|
if (provider.id === "openai" && provider.authType === "oauth" && provider.apiKey) {
|
|
251
278
|
try {
|
|
252
279
|
await this.prepareProvider(provider.id);
|
|
@@ -291,6 +318,25 @@ export class ProviderRegistry {
|
|
|
291
318
|
}));
|
|
292
319
|
}
|
|
293
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* Builtin defaults that were captured into stored profiles before a builtin's
|
|
323
|
+
* baseURL moved. Exactly these values are treated as "not customized" and
|
|
324
|
+
* follow the builtin to its new address (and thereby its new protocol);
|
|
325
|
+
* genuinely custom URLs and profiles with an explicit protocol are untouched.
|
|
326
|
+
*/
|
|
327
|
+
const LEGACY_BUILTIN_BASE_URLS = {
|
|
328
|
+
// google moved from the Gemini OpenAI-compat endpoint to the native API
|
|
329
|
+
// when the "ai-sdk" protocol landed.
|
|
330
|
+
google: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
331
|
+
};
|
|
332
|
+
function upgradeLegacyBaseURL(providerId, baseURL, explicitProtocol) {
|
|
333
|
+
if (explicitProtocol)
|
|
334
|
+
return baseURL;
|
|
335
|
+
const legacy = LEGACY_BUILTIN_BASE_URLS[providerId];
|
|
336
|
+
if (!legacy || normalizeBaseURL(baseURL) !== normalizeBaseURL(legacy))
|
|
337
|
+
return baseURL;
|
|
338
|
+
return getBuiltinProvider(providerId)?.baseURL ?? baseURL;
|
|
339
|
+
}
|
|
294
340
|
function resolveConfiguredProtocol(providerId, baseURL, explicitProtocol) {
|
|
295
341
|
if (explicitProtocol)
|
|
296
342
|
return explicitProtocol;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "./types.js";
|
|
2
|
-
export { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingLevel } from "./variant/variant-resolver.js";
|
|
2
|
+
export { getAvailableThinkingLevels, getDefaultThinkingLevel, isThinkingOnlyLevels, isThinkingOnlyModel, isThinkingToggleModel, normalizeThinkingLevel } from "./variant/variant-resolver.js";
|
|
3
3
|
export interface ProviderRequestConfig {
|
|
4
4
|
effectiveThinkingLevel: ThinkingLevel;
|
|
5
5
|
reasoningEffort?: ThinkingLevel;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { getAvailableThinkingLevels, normalizeThinkingLevel } from "./variant/variant-resolver.js";
|
|
2
|
-
export { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingLevel } from "./variant/variant-resolver.js";
|
|
2
|
+
export { getAvailableThinkingLevels, getDefaultThinkingLevel, isThinkingOnlyLevels, isThinkingOnlyModel, isThinkingToggleModel, normalizeThinkingLevel } from "./variant/variant-resolver.js";
|
|
3
3
|
const MOONSHOT_PROVIDER_IDS = new Set(["moonshot-cn", "moonshot-intl", "kimi-for-coding"]);
|
|
4
|
-
const KIMI_K27_FAMILY = new Set(["kimi-k2.7-code"]);
|
|
5
|
-
const
|
|
6
|
-
const KIMI_THINKING_FAMILY = new Set(["kimi-k2-thinking", "kimi-k2-thinking-turbo"]);
|
|
4
|
+
const KIMI_K27_FAMILY = new Set(["kimi-k2.7-code", "kimi-k2.7-code-highspeed"]);
|
|
5
|
+
const KIMI_TOGGLE_THINKING_FAMILY = new Set(["kimi-k2.5", "kimi-k2.6"]);
|
|
7
6
|
const KIMI_K26_DEFAULT_MAX_TOKENS = 32768;
|
|
8
7
|
const MINIMAX_M3_FAMILY = new Set(["MiniMax-M3"]);
|
|
9
8
|
function isFireworksKimi(providerId, modelId) {
|
|
@@ -99,12 +98,11 @@ export function resolveProviderRequestConfig(providerId, modelId, requestedLevel
|
|
|
99
98
|
},
|
|
100
99
|
};
|
|
101
100
|
}
|
|
102
|
-
// Moonshot / Kimi:
|
|
103
|
-
//
|
|
104
|
-
// kimi-k2-thinking family locks temperature=1.
|
|
101
|
+
// Moonshot / Kimi: K2.5/K2.6 lock temperature/top_p/n/penalties and expose
|
|
102
|
+
// thinking via extra_body.thinking.
|
|
105
103
|
if (MOONSHOT_PROVIDER_IDS.has(providerId)) {
|
|
106
|
-
//
|
|
107
|
-
// (any explicit value errors), thinking can never be disabled, and
|
|
104
|
+
// Kimi K2.7 Code variants are thinking-only: temperature is locked to 1.0
|
|
105
|
+
// server-side (any explicit value errors), thinking can never be disabled, and
|
|
108
106
|
// reasoning_content must be echoed back on tool-call turns.
|
|
109
107
|
if (KIMI_K27_FAMILY.has(modelId)) {
|
|
110
108
|
return {
|
|
@@ -116,7 +114,7 @@ export function resolveProviderRequestConfig(providerId, modelId, requestedLevel
|
|
|
116
114
|
},
|
|
117
115
|
};
|
|
118
116
|
}
|
|
119
|
-
if (
|
|
117
|
+
if (KIMI_TOGGLE_THINKING_FAMILY.has(modelId)) {
|
|
120
118
|
return {
|
|
121
119
|
effectiveThinkingLevel,
|
|
122
120
|
omitTemperature: true,
|
|
@@ -126,9 +124,6 @@ export function resolveProviderRequestConfig(providerId, modelId, requestedLevel
|
|
|
126
124
|
},
|
|
127
125
|
};
|
|
128
126
|
}
|
|
129
|
-
if (KIMI_THINKING_FAMILY.has(modelId)) {
|
|
130
|
-
return { effectiveThinkingLevel, omitTemperature: true };
|
|
131
|
-
}
|
|
132
127
|
return { effectiveThinkingLevel };
|
|
133
128
|
}
|
|
134
129
|
if (providerId === "openai"
|
package/dist/provider.js
CHANGED
|
@@ -7,6 +7,7 @@ import OpenAI from "openai";
|
|
|
7
7
|
import { appendFileSync } from "node:fs";
|
|
8
8
|
import { createAnthropicMessagesProvider } from "./provider-anthropic.js";
|
|
9
9
|
import { createArkResponsesProvider } from "./provider-ark-responses.js";
|
|
10
|
+
import { createAiSdkProvider } from "./provider-ai-sdk.js";
|
|
10
11
|
import { createOpenAICodexProvider, isOpenAICodexBaseUrl } from "./provider-openai-codex.js";
|
|
11
12
|
import { createProviderProtocolArtifactFilter } from "./provider-artifacts.js";
|
|
12
13
|
import { resolveProviderRequestConfig } from "./provider-transform.js";
|
|
@@ -84,6 +85,9 @@ export function createProviderInstance(options) {
|
|
|
84
85
|
if (protocol === "ark-responses") {
|
|
85
86
|
return createArkResponsesProvider(options);
|
|
86
87
|
}
|
|
88
|
+
if (protocol === "ai-sdk") {
|
|
89
|
+
return createAiSdkProvider(options);
|
|
90
|
+
}
|
|
87
91
|
if (isOpenAICodexBaseUrl(options.baseURL)) {
|
|
88
92
|
return createOpenAICodexProvider({
|
|
89
93
|
...options,
|