@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.
Files changed (61) hide show
  1. package/dist/agent/abort-errors.d.ts +2 -2
  2. package/dist/agent/budget-ledger.d.ts +4 -45
  3. package/dist/agent/budget-ledger.js +5 -75
  4. package/dist/agent/child-runner.d.ts +1 -3
  5. package/dist/agent/child-runner.js +6 -37
  6. package/dist/agent/profiles.d.ts +0 -2
  7. package/dist/agent/profiles.js +0 -3
  8. package/dist/agent/subagent-control.d.ts +5 -12
  9. package/dist/agent/subagent-control.js +0 -2
  10. package/dist/agent/subagent-store.d.ts +1 -1
  11. package/dist/agent/subagent-store.js +1 -3
  12. package/dist/agent/workflow/control.d.ts +7 -0
  13. package/dist/agent/workflow/control.js +22 -1
  14. package/dist/agent/workflow/runtime.d.ts +15 -2
  15. package/dist/agent/workflow/runtime.js +112 -30
  16. package/dist/agent.d.ts +7 -43
  17. package/dist/agent.js +50 -176
  18. package/dist/config.d.ts +1 -3
  19. package/dist/config.js +6 -5
  20. package/dist/context/overflow.js +2 -0
  21. package/dist/main.js +9 -1
  22. package/dist/model-catalog.d.ts +1 -1
  23. package/dist/model-catalog.js +25 -18
  24. package/dist/orchestrator/default-hooks.js +7 -1
  25. package/dist/prompt/delegation.d.ts +1 -1
  26. package/dist/prompt/delegation.js +21 -7
  27. package/dist/prompt/task-reminders.d.ts +1 -0
  28. package/dist/prompt/task-reminders.js +24 -1
  29. package/dist/provider-ai-sdk.d.ts +56 -0
  30. package/dist/provider-ai-sdk.js +518 -0
  31. package/dist/provider-registry.js +49 -3
  32. package/dist/provider-transform.d.ts +1 -1
  33. package/dist/provider-transform.js +8 -13
  34. package/dist/provider.js +4 -0
  35. package/dist/slash-commands/commands.js +3 -7
  36. package/dist/slash-commands/types.d.ts +1 -1
  37. package/dist/tools/agent-lifecycle.d.ts +1 -9
  38. package/dist/tools/agent-lifecycle.js +35 -263
  39. package/dist/tui-ink/app.js +51 -13
  40. package/dist/tui-ink/approval/approval-dialog.js +1 -1
  41. package/dist/tui-ink/approval/diff-view.js +4 -4
  42. package/dist/tui-ink/approval/select.js +2 -2
  43. package/dist/tui-ink/feedback-dialog.js +2 -2
  44. package/dist/tui-ink/feishu-setup-picker.js +3 -3
  45. package/dist/tui-ink/input-box.d.ts +1 -6
  46. package/dist/tui-ink/input-box.js +14 -35
  47. package/dist/tui-ink/markdown.js +11 -9
  48. package/dist/tui-ink/message-list.d.ts +5 -1
  49. package/dist/tui-ink/message-list.js +15 -2
  50. package/dist/tui-ink/model-picker.d.ts +3 -1
  51. package/dist/tui-ink/model-picker.js +25 -14
  52. package/dist/tui-ink/plan-confirm.js +1 -1
  53. package/dist/tui-ink/subagent-view.d.ts +3 -1
  54. package/dist/tui-ink/theme.d.ts +19 -2
  55. package/dist/tui-ink/theme.js +25 -8
  56. package/dist/tui-ink/welcome.d.ts +3 -2
  57. package/dist/tui-ink/welcome.js +10 -6
  58. package/dist/types.d.ts +16 -30
  59. package/dist/variant/variant-resolver.d.ts +13 -0
  60. package/dist/variant/variant-resolver.js +26 -0
  61. 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 builtin = getBuiltinProvider(provider.id);
142
+ const baseURL = upgradeLegacyBaseURL(provider.id, provider.baseURL, provider.protocol);
142
143
  return {
143
144
  ...provider,
144
- protocol: resolveConfiguredProtocol(provider.id, provider.baseURL, provider.protocol),
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 KIMI_K25_FAMILY = new Set(["kimi-k2.5", "k2.6-code-preview", "kimi-k2.6"]);
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: kimi-k2.5 family (incl. k2.6-code-preview, kimi-k2.6) locks
103
- // temperature/top_p/n/penalties and exposes thinking via extra_body.thinking;
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
- // kimi-k2.7-code is thinking-only: temperature is locked to 1.0 server-side
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 (KIMI_K25_FAMILY.has(modelId)) {
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,