@bubblebrain-ai/bubble 0.0.34 → 0.0.36

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 (168) hide show
  1. package/README.md +27 -7
  2. package/dist/agent/abort-errors.d.ts +2 -2
  3. package/dist/agent/budget-ledger.d.ts +4 -45
  4. package/dist/agent/budget-ledger.js +5 -75
  5. package/dist/agent/categories.d.ts +66 -2
  6. package/dist/agent/categories.js +120 -19
  7. package/dist/agent/child-runner.d.ts +1 -3
  8. package/dist/agent/child-runner.js +6 -37
  9. package/dist/agent/discovery-barrier.js +1 -1
  10. package/dist/agent/profiles.d.ts +1 -2
  11. package/dist/agent/profiles.js +25 -14
  12. package/dist/agent/routing-catalog.d.ts +113 -0
  13. package/dist/agent/routing-catalog.js +240 -0
  14. package/dist/agent/subagent-control.d.ts +5 -12
  15. package/dist/agent/subagent-control.js +0 -2
  16. package/dist/agent/subagent-store.d.ts +1 -1
  17. package/dist/agent/subagent-store.js +1 -3
  18. package/dist/agent/workflow/control.d.ts +21 -0
  19. package/dist/agent/workflow/control.js +50 -8
  20. package/dist/agent/workflow/runtime.d.ts +15 -2
  21. package/dist/agent/workflow/runtime.js +112 -30
  22. package/dist/agent.d.ts +82 -48
  23. package/dist/agent.js +360 -232
  24. package/dist/approval/controller.js +28 -0
  25. package/dist/approval/tool-helper.js +2 -0
  26. package/dist/approval/types.d.ts +21 -1
  27. package/dist/cli.d.ts +1 -1
  28. package/dist/cli.js +2 -1
  29. package/dist/config.d.ts +5 -3
  30. package/dist/config.js +10 -5
  31. package/dist/context/compact-llm.d.ts +2 -2
  32. package/dist/external-runtime/grok-acp.d.ts +56 -0
  33. package/dist/external-runtime/grok-acp.js +386 -0
  34. package/dist/external-runtime/grok-binary.d.ts +25 -0
  35. package/dist/external-runtime/grok-binary.js +108 -0
  36. package/dist/external-runtime/grok-errors.d.ts +7 -0
  37. package/dist/external-runtime/grok-errors.js +15 -0
  38. package/dist/external-runtime/grok-input-policy.d.ts +58 -0
  39. package/dist/external-runtime/grok-input-policy.js +52 -0
  40. package/dist/external-runtime/grok-ndjson.d.ts +14 -0
  41. package/dist/external-runtime/grok-ndjson.js +193 -0
  42. package/dist/external-runtime/grok-network.d.ts +15 -0
  43. package/dist/external-runtime/grok-network.js +81 -0
  44. package/dist/external-runtime/grok-preflight.d.ts +28 -0
  45. package/dist/external-runtime/grok-preflight.js +194 -0
  46. package/dist/external-runtime/grok-process.d.ts +26 -0
  47. package/dist/external-runtime/grok-process.js +184 -0
  48. package/dist/external-runtime/grok-profile.d.ts +32 -0
  49. package/dist/external-runtime/grok-profile.js +419 -0
  50. package/dist/external-runtime/grok-provider.d.ts +31 -0
  51. package/dist/external-runtime/grok-provider.js +41 -0
  52. package/dist/external-runtime/grok-runtime.d.ts +91 -0
  53. package/dist/external-runtime/grok-runtime.js +882 -0
  54. package/dist/external-runtime/index.d.ts +13 -0
  55. package/dist/external-runtime/index.js +13 -0
  56. package/dist/external-runtime/session-policy.d.ts +11 -0
  57. package/dist/external-runtime/session-policy.js +30 -0
  58. package/dist/external-runtime/types.d.ts +65 -0
  59. package/dist/external-runtime/types.js +1 -0
  60. package/dist/feishu/agent-host/approval-card.js +21 -0
  61. package/dist/feishu/agent-host/run-driver.js +19 -3
  62. package/dist/feishu/serve.js +1 -0
  63. package/dist/goal/tools.d.ts +6 -3
  64. package/dist/goal/tools.js +7 -19
  65. package/dist/main.js +114 -28
  66. package/dist/memory/db.d.ts +2 -0
  67. package/dist/memory/db.js +10 -0
  68. package/dist/memory/index.d.ts +1 -0
  69. package/dist/memory/index.js +1 -0
  70. package/dist/memory/phase1.js +14 -2
  71. package/dist/memory/phase2.js +21 -2
  72. package/dist/memory/prompts.js +1 -1
  73. package/dist/memory/session-policy.d.ts +25 -0
  74. package/dist/memory/session-policy.js +55 -0
  75. package/dist/memory/storage.d.ts +1 -0
  76. package/dist/memory/storage.js +13 -0
  77. package/dist/model-catalog.d.ts +18 -0
  78. package/dist/model-catalog.js +92 -52
  79. package/dist/model-config.d.ts +3 -1
  80. package/dist/model-config.js +11 -0
  81. package/dist/oauth/grok.d.ts +25 -0
  82. package/dist/oauth/grok.js +218 -0
  83. package/dist/oauth/index.d.ts +1 -0
  84. package/dist/oauth/index.js +1 -0
  85. package/dist/oauth/storage.d.ts +9 -0
  86. package/dist/oauth/storage.js +25 -0
  87. package/dist/orchestrator/default-hooks.js +23 -7
  88. package/dist/prompt/compose.d.ts +3 -0
  89. package/dist/prompt/compose.js +3 -0
  90. package/dist/prompt/delegation.d.ts +1 -1
  91. package/dist/prompt/delegation.js +41 -8
  92. package/dist/prompt/environment.js +0 -4
  93. package/dist/prompt/reminders.d.ts +0 -1
  94. package/dist/prompt/reminders.js +1 -11
  95. package/dist/prompt/routing.d.ts +14 -0
  96. package/dist/prompt/routing.js +71 -0
  97. package/dist/prompt/runtime.js +1 -1
  98. package/dist/prompt/task-reminders.d.ts +20 -0
  99. package/dist/prompt/task-reminders.js +73 -1
  100. package/dist/provider-ai-sdk.js +2 -2
  101. package/dist/provider-grok.d.ts +26 -0
  102. package/dist/provider-grok.js +59 -0
  103. package/dist/provider-openai-codex.d.ts +17 -1
  104. package/dist/provider-openai-codex.js +101 -29
  105. package/dist/provider-registry.d.ts +72 -0
  106. package/dist/provider-registry.js +336 -59
  107. package/dist/provider-transform.d.ts +1 -1
  108. package/dist/provider-transform.js +35 -17
  109. package/dist/provider.d.ts +3 -0
  110. package/dist/provider.js +14 -0
  111. package/dist/session-types.d.ts +19 -1
  112. package/dist/session.d.ts +1 -0
  113. package/dist/session.js +4 -0
  114. package/dist/slash-commands/commands.js +224 -14
  115. package/dist/slash-commands/types.d.ts +14 -1
  116. package/dist/system-prompt.d.ts +2 -0
  117. package/dist/tools/agent-lifecycle.d.ts +1 -9
  118. package/dist/tools/agent-lifecycle.js +42 -272
  119. package/dist/tools/bash.js +1 -1
  120. package/dist/tools/child-tools.js +2 -0
  121. package/dist/tools/index.d.ts +2 -2
  122. package/dist/tools/index.js +7 -5
  123. package/dist/tools/lsp.js +1 -0
  124. package/dist/tools/memory.d.ts +7 -2
  125. package/dist/tools/memory.js +28 -43
  126. package/dist/tools/server.js +4 -0
  127. package/dist/tools/tool-search.d.ts +1 -1
  128. package/dist/tools/tool-search.js +23 -3
  129. package/dist/tui/model-switch.d.ts +7 -0
  130. package/dist/tui/model-switch.js +15 -5
  131. package/dist/tui-ink/app.d.ts +3 -1
  132. package/dist/tui-ink/app.js +672 -243
  133. package/dist/tui-ink/approval/approval-dialog.js +37 -1
  134. package/dist/tui-ink/approval/diff-view.js +4 -4
  135. package/dist/tui-ink/approval/select.js +2 -2
  136. package/dist/tui-ink/composer-buffer.d.ts +55 -0
  137. package/dist/tui-ink/composer-buffer.js +317 -0
  138. package/dist/tui-ink/feedback-dialog.js +2 -2
  139. package/dist/tui-ink/feishu-setup-picker.js +3 -3
  140. package/dist/tui-ink/footer.d.ts +1 -0
  141. package/dist/tui-ink/footer.js +7 -2
  142. package/dist/tui-ink/input-box.d.ts +36 -7
  143. package/dist/tui-ink/input-box.js +229 -157
  144. package/dist/tui-ink/markdown.js +11 -9
  145. package/dist/tui-ink/message-list.d.ts +5 -1
  146. package/dist/tui-ink/message-list.js +15 -2
  147. package/dist/tui-ink/model-picker.d.ts +61 -4
  148. package/dist/tui-ink/model-picker.js +464 -128
  149. package/dist/tui-ink/paste-operation-tracker.d.ts +13 -0
  150. package/dist/tui-ink/paste-operation-tracker.js +25 -0
  151. package/dist/tui-ink/plan-confirm.js +1 -1
  152. package/dist/tui-ink/run.d.ts +3 -0
  153. package/dist/tui-ink/run.js +1 -1
  154. package/dist/tui-ink/subagent-view.d.ts +34 -1
  155. package/dist/tui-ink/subagent-view.js +82 -1
  156. package/dist/tui-ink/theme.d.ts +19 -2
  157. package/dist/tui-ink/theme.js +25 -8
  158. package/dist/tui-ink/welcome.d.ts +3 -2
  159. package/dist/tui-ink/welcome.js +10 -6
  160. package/dist/types.d.ts +8 -31
  161. package/dist/types.js +1 -1
  162. package/dist/variant/thinking-level.d.ts +2 -2
  163. package/dist/variant/thinking-level.js +2 -1
  164. package/dist/variant/variant-resolver.d.ts +22 -0
  165. package/dist/variant/variant-resolver.js +43 -0
  166. package/package.json +6 -2
  167. package/dist/tools/task.d.ts +0 -2
  168. package/dist/tools/task.js +0 -58
@@ -4,12 +4,16 @@
4
4
  * Supports OpenAI-compatible providers with dynamic or static model lists.
5
5
  * Reads provider configuration from models.json first, then falls back to config.json.
6
6
  */
7
- import { BUILTIN_PROVIDERS as CATALOG_PROVIDERS, getBuiltinModel, getBuiltinProvider, listBuiltinModels, registerDynamicModelMetadata, } from "./model-catalog.js";
7
+ import { createHash } from "node:crypto";
8
+ import { BUILTIN_PROVIDERS as CATALOG_PROVIDERS, clearDynamicModelMetadata, getBuiltinModel, getBuiltinProvider, listBuiltinModels, replaceDynamicModelMetadata, } from "./model-catalog.js";
8
9
  import { ModelConfig } from "./model-config.js";
9
10
  import { AuthStorage } from "./oauth/index.js";
10
11
  import { fetchGeminiModels } from "./provider-ai-sdk.js";
11
- import { fetchOpenAICodexModels } from "./provider-openai-codex.js";
12
+ import { extractChatGptAccountId, fetchOpenAICodexModelCatalog } from "./provider-openai-codex.js";
12
13
  import { refreshOpenAICodex } from "./oauth/openai-codex.js";
14
+ import { refreshGrok } from "./oauth/grok.js";
15
+ const MODEL_DISCOVERY_SUCCESS_TTL_MS = 60_000;
16
+ const MODEL_DISCOVERY_FAILURE_TTL_MS = 10_000;
13
17
  export const BUILTIN_PROVIDERS = CATALOG_PROVIDERS;
14
18
  export const USER_VISIBLE_PROVIDER_IDS = BUILTIN_PROVIDERS
15
19
  .filter((provider) => !provider.hidden && provider.id !== "openrouter" && provider.id !== "openai-codex")
@@ -21,10 +25,59 @@ export class ProviderRegistry {
21
25
  config;
22
26
  modelConfig;
23
27
  authStorage;
28
+ modelDiscoveryCache = new Map();
29
+ modelDiscoveryInFlight = new Map();
30
+ modelDiscoveryGeneration = new Map();
31
+ /** Last membership seen per discovery key — survives cache eviction/TTL so
32
+ * a re-discovery with identical membership never bumps the revision. */
33
+ lastDiscoveryMembership = new Map();
34
+ routingRevision = 0;
35
+ identityFingerprints = new Map();
24
36
  constructor(config) {
25
37
  this.config = config;
26
38
  this.modelConfig = new ModelConfig();
27
39
  this.authStorage = new AuthStorage();
40
+ for (const authKey of this.authStorage.list()) {
41
+ const fingerprint = this.credentialFingerprint(authKey);
42
+ if (fingerprint)
43
+ this.identityFingerprints.set(authKey, fingerprint);
44
+ }
45
+ this.authStorage.onMutation((authKey) => this.handleAuthMutation(authKey));
46
+ }
47
+ /**
48
+ * Monotonic counter of semantic routing-world changes: provider set,
49
+ * credential identity, provider key/config, discovery membership.
50
+ * Same-account OAuth token rotation deliberately does NOT bump it
51
+ * (docs/model-routing-design.md §1.6).
52
+ */
53
+ getRoutingRevision() {
54
+ return this.routingRevision;
55
+ }
56
+ bumpRoutingRevision() {
57
+ this.routingRevision++;
58
+ }
59
+ /** Stable identity for stored credentials: account id, else refresh-token hash. */
60
+ credentialFingerprint(authKey) {
61
+ const credentials = this.authStorage.get(authKey);
62
+ if (!credentials)
63
+ return undefined;
64
+ if (credentials.accountId)
65
+ return credentials.accountId;
66
+ const stable = credentials.refreshToken || credentials.accessToken;
67
+ return createHash("sha256").update(stable).digest("hex").slice(0, 16);
68
+ }
69
+ handleAuthMutation(authKey) {
70
+ const next = this.credentialFingerprint(authKey);
71
+ const prev = this.identityFingerprints.get(authKey);
72
+ if (next === prev)
73
+ return; // same-identity token rotation: not a routing change
74
+ if (next === undefined) {
75
+ this.identityFingerprints.delete(authKey);
76
+ }
77
+ else {
78
+ this.identityFingerprints.set(authKey, next);
79
+ }
80
+ this.bumpRoutingRevision();
28
81
  }
29
82
  getModelConfig() {
30
83
  return this.modelConfig;
@@ -84,6 +137,37 @@ export class ProviderRegistry {
84
137
  },
85
138
  };
86
139
  }
140
+ createGrokAuthAdapter(providerId) {
141
+ if (providerId !== "grok" || !this.authStorage.has("grok"))
142
+ return undefined;
143
+ let refreshPromise;
144
+ return {
145
+ getCredentials: () => this.authStorage.get("grok"),
146
+ isExpired: (_credentials, graceMs) => this.authStorage.isExpired("grok", graceMs),
147
+ refreshCredentials: async () => {
148
+ if (!refreshPromise) {
149
+ refreshPromise = (async () => {
150
+ const current = this.authStorage.get("grok");
151
+ if (!current?.refreshToken) {
152
+ throw new Error("Grok OAuth credentials are missing a refresh token. Run /login grok again.");
153
+ }
154
+ const refreshed = await refreshGrok(current.refreshToken);
155
+ const next = {
156
+ type: "oauth",
157
+ accessToken: refreshed.accessToken,
158
+ refreshToken: refreshed.refreshToken,
159
+ expiresAt: refreshed.expiresAt,
160
+ };
161
+ this.authStorage.set("grok", next);
162
+ return next;
163
+ })().finally(() => {
164
+ refreshPromise = undefined;
165
+ });
166
+ }
167
+ return refreshPromise;
168
+ },
169
+ };
170
+ }
87
171
  getDefaultModel(providerId, authType = "api") {
88
172
  const customModels = this.modelConfig.getCustomModels(providerId);
89
173
  if (customModels.length > 0) {
@@ -95,6 +179,19 @@ export class ProviderRegistry {
95
179
  return listBuiltinModels(providerId)[0]?.id;
96
180
  }
97
181
  async prepareProvider(providerId) {
182
+ if (providerId === "grok" && this.authStorage.isExpired("grok")) {
183
+ const creds = this.authStorage.get("grok");
184
+ if (creds?.refreshToken) {
185
+ const refreshed = await refreshGrok(creds.refreshToken);
186
+ this.authStorage.set("grok", {
187
+ type: "oauth",
188
+ accessToken: refreshed.accessToken,
189
+ refreshToken: refreshed.refreshToken,
190
+ expiresAt: refreshed.expiresAt,
191
+ });
192
+ }
193
+ return;
194
+ }
98
195
  const authKey = this.resolveOAuthAuthKey(providerId);
99
196
  if ((providerId === "openai" || providerId === "openai-codex") && this.authStorage.isExpired(authKey)) {
100
197
  const creds = this.authStorage.get(authKey);
@@ -213,11 +310,13 @@ export class ProviderRegistry {
213
310
  providers.push(profile);
214
311
  }
215
312
  this.config.setProviders(providers);
313
+ this.bumpRoutingRevision();
216
314
  return true;
217
315
  }
218
316
  removeProvider(id) {
219
317
  const providers = this.config.getProviders().filter((p) => p.id !== id);
220
318
  this.config.setProviders(providers);
319
+ this.bumpRoutingRevision();
221
320
  }
222
321
  updateProviderKey(id, apiKey) {
223
322
  const providers = this.config.getProviders();
@@ -225,28 +324,92 @@ export class ProviderRegistry {
225
324
  if (p) {
226
325
  p.apiKey = apiKey;
227
326
  this.config.setProviders(providers);
327
+ this.bumpRoutingRevision();
228
328
  }
229
329
  }
230
330
  async listModels(provider) {
231
- // 1. Custom models from models.json always take precedence
331
+ return (await this.discoverModels(provider)).models;
332
+ }
333
+ async discoverModels(provider, options = {}) {
334
+ const key = this.modelDiscoveryKey(provider);
335
+ const now = Date.now();
336
+ if (!options.forceRefresh) {
337
+ const cached = this.modelDiscoveryCache.get(key);
338
+ if (cached && cached.expiresAt > now) {
339
+ const result = { ...cached.result, source: "cache" };
340
+ const current = this.getConfigured().find((item) => item.id === provider.id);
341
+ if (!current || this.modelDiscoveryKey(current) === key) {
342
+ this.applyDynamicDiscoveryMetadata(provider, result);
343
+ }
344
+ return result;
345
+ }
346
+ const inFlight = this.modelDiscoveryInFlight.get(key);
347
+ if (inFlight)
348
+ return inFlight;
349
+ }
350
+ else {
351
+ this.modelDiscoveryCache.delete(key);
352
+ // Do not let a superseded request's finally-handler remove this retry.
353
+ this.modelDiscoveryInFlight.delete(key);
354
+ }
355
+ const generation = (this.modelDiscoveryGeneration.get(provider.id) ?? 0) + 1;
356
+ this.modelDiscoveryGeneration.set(provider.id, generation);
357
+ let pending;
358
+ pending = this.performModelDiscovery(provider).then((result) => {
359
+ if (!this.isCurrentModelDiscovery(provider, key, generation)) {
360
+ return {
361
+ models: this.localModelsForProvider(this.getConfigured().find((item) => item.id === provider.id) ?? provider),
362
+ source: "fallback",
363
+ authoritative: false,
364
+ error: "Model catalog changed while it was refreshing.",
365
+ };
366
+ }
367
+ this.applyDynamicDiscoveryMetadata(provider, result);
368
+ const previous = this.lastDiscoveryMembership.get(key);
369
+ this.modelDiscoveryCache.set(key, {
370
+ result: { ...result, source: result.source === "cache" ? "remote" : result.source },
371
+ expiresAt: Date.now() + (result.error ? MODEL_DISCOVERY_FAILURE_TTL_MS : MODEL_DISCOVERY_SUCCESS_TTL_MS),
372
+ identityKey: this.discoveryIdentity(provider),
373
+ });
374
+ this.lastDiscoveryMembership.set(key, {
375
+ ids: result.models.map((model) => model.id),
376
+ authoritative: result.authoritative,
377
+ });
378
+ if (discoveryMembershipChanged(previous, result)) {
379
+ this.bumpRoutingRevision();
380
+ }
381
+ return result;
382
+ }).finally(() => {
383
+ if (this.modelDiscoveryInFlight.get(key) === pending) {
384
+ this.modelDiscoveryInFlight.delete(key);
385
+ }
386
+ });
387
+ this.modelDiscoveryInFlight.set(key, pending);
388
+ return pending;
389
+ }
390
+ async performModelDiscovery(provider) {
232
391
  const customModels = this.modelConfig.getCustomModels(provider.id);
233
392
  if (customModels.length > 0) {
234
- return customModels;
393
+ return { models: customModels, source: "static", authoritative: true };
235
394
  }
236
- // 2. Built-in provider dynamic/static lists
237
395
  if (provider.id === "openrouter") {
238
396
  try {
239
- const res = await fetch("https://openrouter.ai/api/v1/models");
240
- const data = (await res.json());
241
- const models = data.data || [];
242
- return models.map((m) => ({
243
- id: m.id,
244
- name: m.name || m.id,
245
- providerId: provider.id,
246
- }));
397
+ const response = await fetch("https://openrouter.ai/api/v1/models");
398
+ if (!response.ok)
399
+ throw new Error(`HTTP ${response.status}`);
400
+ const data = (await response.json());
401
+ return {
402
+ models: (data.data ?? []).map((model) => ({
403
+ id: model.id,
404
+ name: model.name || model.id,
405
+ providerId: provider.id,
406
+ })),
407
+ source: "remote",
408
+ authoritative: true,
409
+ };
247
410
  }
248
- catch {
249
- // fall through to static
411
+ catch (error) {
412
+ return this.fallbackDiscovery(provider, error);
250
413
  }
251
414
  }
252
415
  if (provider.id === "google" && provider.protocol === "ai-sdk" && provider.apiKey) {
@@ -255,68 +418,182 @@ export class ProviderRegistry {
255
418
  apiKey: provider.apiKey,
256
419
  baseURL: provider.baseURL,
257
420
  });
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
- }
421
+ return {
422
+ models: descriptors.map((descriptor) => {
423
+ const catalogEntry = getBuiltinModel("google", descriptor.id);
424
+ return {
425
+ id: descriptor.id,
426
+ name: descriptor.name,
427
+ providerId: provider.id,
428
+ reasoningLevels: descriptor.reasoningLevels,
429
+ defaultReasoningLevel: descriptor.defaultReasoningLevel ?? catalogEntry?.defaultReasoningLevel,
430
+ contextWindow: descriptor.contextWindow ?? catalogEntry?.contextWindow,
431
+ };
432
+ }),
433
+ source: "remote",
434
+ authoritative: true,
435
+ };
272
436
  }
273
- catch {
274
- // fall through to static
437
+ catch (error) {
438
+ return this.fallbackDiscovery(provider, error);
275
439
  }
276
440
  }
277
441
  if (provider.id === "openai" && provider.authType === "oauth" && provider.apiKey) {
278
442
  try {
279
443
  await this.prepareProvider(provider.id);
280
- const currentProvider = this.getConfigured().find((p) => p.id === provider.id) || provider;
281
- const descriptors = await fetchOpenAICodexModels({
444
+ const currentProvider = this.getConfigured().find((item) => item.id === provider.id) ?? provider;
445
+ const catalog = await fetchOpenAICodexModelCatalog({
282
446
  baseURL: currentProvider.baseURL,
283
447
  accessToken: currentProvider.apiKey,
284
448
  });
285
- const visible = descriptors.filter((d) => d.visibility !== "hide");
286
- if (visible.length > 0) {
287
- for (const d of visible) {
288
- const catalogEntry = getBuiltinModel("openai-codex", d.id);
289
- registerDynamicModelMetadata({
290
- id: d.id,
291
- name: d.displayName || catalogEntry?.name || d.id,
292
- providerId: "openai-codex",
293
- reasoningLevels: d.reasoningLevels ?? catalogEntry?.reasoningLevels ?? ["off"],
294
- contextWindow: d.contextWindow ?? catalogEntry?.contextWindow,
295
- toolOutputTokenLimit: d.toolOutputTokenLimit ?? catalogEntry?.toolOutputTokenLimit,
296
- });
297
- }
298
- return visible.map((d) => ({
299
- id: d.id,
300
- name: d.displayName || d.id,
301
- providerId: provider.id,
302
- }));
449
+ if (catalog.status === "unavailable") {
450
+ throw new Error("OpenAI Codex model catalog is unavailable.");
303
451
  }
452
+ const visible = catalog.descriptors.filter((descriptor) => descriptor.visibility !== "hide");
453
+ return {
454
+ models: visible.map((descriptor) => {
455
+ const catalogEntry = getBuiltinModel("openai-codex", descriptor.id);
456
+ return {
457
+ id: descriptor.id,
458
+ name: descriptor.displayName || catalogEntry?.name || descriptor.id,
459
+ providerId: provider.id,
460
+ reasoningLevels: descriptor.reasoningLevels ?? catalogEntry?.reasoningLevels,
461
+ defaultReasoningLevel: descriptor.defaultReasoningLevel ?? catalogEntry?.defaultReasoningLevel,
462
+ contextWindow: descriptor.contextWindow ?? catalogEntry?.contextWindow,
463
+ useResponsesLite: descriptor.useResponsesLite ?? catalogEntry?.useResponsesLite,
464
+ toolOutputTokenLimit: descriptor.toolOutputTokenLimit ?? catalogEntry?.toolOutputTokenLimit,
465
+ };
466
+ }),
467
+ source: "remote",
468
+ authoritative: true,
469
+ };
304
470
  }
305
- catch {
306
- // fall through to static
471
+ catch (error) {
472
+ return this.fallbackDiscovery(provider, error);
307
473
  }
308
- return listBuiltinModels("openai-codex").map((model) => ({
309
- id: model.id,
310
- name: model.name,
311
- providerId: provider.id,
312
- }));
313
474
  }
314
- return listBuiltinModels(provider.id).map((model) => ({
475
+ return {
476
+ models: this.localModelsForProvider(provider),
477
+ source: "static",
478
+ authoritative: true,
479
+ };
480
+ }
481
+ fallbackDiscovery(provider, error) {
482
+ return {
483
+ models: this.localModelsForProvider(provider),
484
+ source: "fallback",
485
+ authoritative: false,
486
+ error: modelDiscoveryError(error),
487
+ };
488
+ }
489
+ localModelsForProvider(provider) {
490
+ const customModels = this.modelConfig.getCustomModels(provider.id);
491
+ if (customModels.length > 0)
492
+ return customModels;
493
+ const catalogProviderId = provider.id === "openai" && provider.authType === "oauth"
494
+ ? "openai-codex"
495
+ : provider.id;
496
+ return listBuiltinModels(catalogProviderId).map((model) => ({
315
497
  id: model.id,
316
498
  name: model.name,
317
499
  providerId: provider.id,
500
+ reasoningLevels: model.reasoningLevels,
501
+ defaultReasoningLevel: model.defaultReasoningLevel,
502
+ contextWindow: model.contextWindow,
503
+ useResponsesLite: model.useResponsesLite,
504
+ toolOutputTokenLimit: model.toolOutputTokenLimit,
318
505
  }));
319
506
  }
507
+ discoveryIdentity(provider) {
508
+ if (provider.id === "openai" && provider.authType === "oauth") {
509
+ const credentials = this.authStorage.get(this.resolveOAuthAuthKey(provider.id));
510
+ return credentials?.accountId
511
+ || extractChatGptAccountId(provider.apiKey)
512
+ || "unknown-account";
513
+ }
514
+ if (provider.apiKey) {
515
+ return createHash("sha256").update(provider.apiKey).digest("hex").slice(0, 16);
516
+ }
517
+ return "anonymous";
518
+ }
519
+ modelDiscoveryKey(provider) {
520
+ return JSON.stringify([
521
+ provider.id,
522
+ normalizeBaseURL(provider.baseURL),
523
+ provider.authType ?? "api",
524
+ provider.protocol ?? "default",
525
+ this.discoveryIdentity(provider),
526
+ ]);
527
+ }
528
+ /**
529
+ * Read-only view of the cached discovery result for the provider's CURRENT
530
+ * configuration/identity (docs/model-routing-design.md §1.4). Returns
531
+ * undefined when nothing is cached or the cache has expired. `complete` is
532
+ * derived strictly: only a successful full remote catalog qualifies —
533
+ * never static or fallback results, whose internal `authoritative` flag
534
+ * this design does not trust.
535
+ */
536
+ getCachedDiscoverySnapshot(providerId) {
537
+ const provider = this.getConfigured().find((item) => item.id === providerId);
538
+ if (!provider)
539
+ return undefined;
540
+ const cached = this.modelDiscoveryCache.get(this.modelDiscoveryKey(provider));
541
+ if (!cached || cached.expiresAt <= Date.now())
542
+ return undefined;
543
+ return {
544
+ models: cached.result.models,
545
+ source: cached.result.source,
546
+ complete: cached.result.source === "remote" && cached.result.authoritative && !cached.result.error,
547
+ expiresAt: cached.expiresAt,
548
+ identityKey: cached.identityKey,
549
+ };
550
+ }
551
+ isCurrentModelDiscovery(provider, key, generation) {
552
+ if (this.modelDiscoveryGeneration.get(provider.id) !== generation)
553
+ return false;
554
+ const current = this.getConfigured().find((item) => item.id === provider.id);
555
+ return !current || this.modelDiscoveryKey(current) === key;
556
+ }
557
+ applyDynamicDiscoveryMetadata(provider, result) {
558
+ const dynamicProviderId = provider.id === "openai" && provider.authType === "oauth"
559
+ ? "openai-codex"
560
+ : provider.id === "google" && provider.protocol === "ai-sdk"
561
+ ? "google"
562
+ : undefined;
563
+ if (!dynamicProviderId)
564
+ return;
565
+ if (!result.authoritative) {
566
+ clearDynamicModelMetadata(dynamicProviderId);
567
+ return;
568
+ }
569
+ replaceDynamicModelMetadata(dynamicProviderId, result.models.map((model) => ({
570
+ id: model.id,
571
+ name: model.name,
572
+ providerId: dynamicProviderId,
573
+ // Empty means the remote model exists but declared no trusted effort metadata.
574
+ reasoningLevels: model.reasoningLevels ?? [],
575
+ defaultReasoningLevel: model.defaultReasoningLevel,
576
+ contextWindow: model.contextWindow,
577
+ useResponsesLite: model.useResponsesLite,
578
+ toolOutputTokenLimit: model.toolOutputTokenLimit,
579
+ })));
580
+ }
581
+ }
582
+ function modelDiscoveryError(error) {
583
+ if (error instanceof Error && error.message.trim())
584
+ return error.message;
585
+ return "Remote model catalog is unavailable.";
586
+ }
587
+ /** True when a discovery write changes the routing world: member ids or completeness. */
588
+ function discoveryMembershipChanged(previous, next) {
589
+ if (!previous)
590
+ return true;
591
+ if (previous.authoritative !== next.authoritative)
592
+ return true;
593
+ const previousIds = new Set(previous.ids);
594
+ if (previousIds.size !== next.models.length)
595
+ return true;
596
+ return next.models.some((model) => !previousIds.has(model.id));
320
597
  }
321
598
  /**
322
599
  * Builtin defaults that were captured into stored profiles before a builtin's
@@ -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, normalizeInheritedThinkingLevel, normalizeThinkingLevel } from "./variant/variant-resolver.js";
3
3
  export interface ProviderRequestConfig {
4
4
  effectiveThinkingLevel: ThinkingLevel;
5
5
  reasoningEffort?: ThinkingLevel;
@@ -1,9 +1,9 @@
1
- import { getAvailableThinkingLevels, normalizeThinkingLevel } from "./variant/variant-resolver.js";
2
- export { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingLevel } from "./variant/variant-resolver.js";
1
+ import { getBuiltinModel } from "./model-catalog.js";
2
+ import { getAvailableThinkingLevels, normalizeInheritedThinkingLevel, normalizeThinkingLevel } from "./variant/variant-resolver.js";
3
+ export { getAvailableThinkingLevels, getDefaultThinkingLevel, isThinkingOnlyLevels, isThinkingOnlyModel, isThinkingToggleModel, normalizeInheritedThinkingLevel, normalizeThinkingLevel } from "./variant/variant-resolver.js";
3
4
  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"]);
5
+ const KIMI_K27_FAMILY = new Set(["kimi-k2.7-code", "kimi-k2.7-code-highspeed"]);
6
+ const KIMI_TOGGLE_THINKING_FAMILY = new Set(["kimi-k2.5", "kimi-k2.6"]);
7
7
  const KIMI_K26_DEFAULT_MAX_TOKENS = 32768;
8
8
  const MINIMAX_M3_FAMILY = new Set(["MiniMax-M3"]);
9
9
  function isFireworksKimi(providerId, modelId) {
@@ -15,10 +15,32 @@ function isFireworksKimi(providerId, modelId) {
15
15
  export function resolveProviderRequestConfig(providerId, modelId, requestedLevel) {
16
16
  const supportedLevels = getAvailableThinkingLevels(providerId, modelId);
17
17
  const effectiveThinkingLevel = normalizeThinkingLevel(requestedLevel, supportedLevels);
18
- // ChatGPT OAuth via openai-codex currently rejects explicit reasoning params for this account path.
19
- // Keep the session/UI state, but don't send reasoning flags on this provider until the protocol is clearer.
20
18
  if (providerId === "openai-codex") {
21
- return { effectiveThinkingLevel };
19
+ const model = getBuiltinModel(providerId, modelId);
20
+ if (!model || model.reasoningLevels.length === 0) {
21
+ return { effectiveThinkingLevel: "off" };
22
+ }
23
+ // Provider serialization is the final defensive boundary. Upstream model
24
+ // selection validates explicit choices, while inherited stale state falls
25
+ // back to the model's declared default here rather than leaking an invalid
26
+ // effort to the ChatGPT Responses endpoint.
27
+ const codexThinkingLevel = normalizeInheritedThinkingLevel(providerId, modelId, requestedLevel);
28
+ return {
29
+ effectiveThinkingLevel: codexThinkingLevel,
30
+ reasoningEffort: codexThinkingLevel !== "off" && model.reasoningLevels.includes(codexThinkingLevel)
31
+ ? codexThinkingLevel
32
+ : undefined,
33
+ };
34
+ }
35
+ // Grok's CLI chat proxy is OpenAI-compatible; effort rides in the body as
36
+ // `reasoning_effort`, and reasoning arrives back as `reasoning_content`.
37
+ if (providerId === "grok") {
38
+ return {
39
+ effectiveThinkingLevel,
40
+ extraBody: effectiveThinkingLevel === "off"
41
+ ? undefined
42
+ : { reasoning_effort: effectiveThinkingLevel },
43
+ };
22
44
  }
23
45
  if (isFireworksKimi(providerId, modelId)) {
24
46
  return {
@@ -99,12 +121,11 @@ export function resolveProviderRequestConfig(providerId, modelId, requestedLevel
99
121
  },
100
122
  };
101
123
  }
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.
124
+ // Moonshot / Kimi: K2.5/K2.6 lock temperature/top_p/n/penalties and expose
125
+ // thinking via extra_body.thinking.
105
126
  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
127
+ // Kimi K2.7 Code variants are thinking-only: temperature is locked to 1.0
128
+ // server-side (any explicit value errors), thinking can never be disabled, and
108
129
  // reasoning_content must be echoed back on tool-call turns.
109
130
  if (KIMI_K27_FAMILY.has(modelId)) {
110
131
  return {
@@ -116,7 +137,7 @@ export function resolveProviderRequestConfig(providerId, modelId, requestedLevel
116
137
  },
117
138
  };
118
139
  }
119
- if (KIMI_K25_FAMILY.has(modelId)) {
140
+ if (KIMI_TOGGLE_THINKING_FAMILY.has(modelId)) {
120
141
  return {
121
142
  effectiveThinkingLevel,
122
143
  omitTemperature: true,
@@ -126,9 +147,6 @@ export function resolveProviderRequestConfig(providerId, modelId, requestedLevel
126
147
  },
127
148
  };
128
149
  }
129
- if (KIMI_THINKING_FAMILY.has(modelId)) {
130
- return { effectiveThinkingLevel, omitTemperature: true };
131
- }
132
150
  return { effectiveThinkingLevel };
133
151
  }
134
152
  if (providerId === "openai"
@@ -4,6 +4,7 @@
4
4
  * Works with OpenRouter, OpenAI, DeepSeek, Google, Groq, Together, and local OpenAI-compatible endpoints.
5
5
  */
6
6
  import { type OpenAICodexAuthAdapter } from "./provider-openai-codex.js";
7
+ import { type GrokAuthAdapter } from "./provider-grok.js";
7
8
  import type { ProviderProtocol } from "./model-catalog.js";
8
9
  import type { Provider, ProviderMessage, StreamChunk, ThinkingLevel } from "./types.js";
9
10
  type ReasoningContentEcho = "tool_calls" | "all" | "none" | "minimax";
@@ -29,6 +30,8 @@ export interface ProviderInstanceOptions {
29
30
  protocol?: ProviderProtocol;
30
31
  /** Dynamic OAuth access-token loader/refresh hook for ChatGPT Codex requests. */
31
32
  openAICodexAuth?: OpenAICodexAuthAdapter;
33
+ /** Dynamic OAuth access-token loader/refresh hook for Grok subscription requests. */
34
+ grokAuth?: GrokAuthAdapter;
32
35
  }
33
36
  export declare function createUnavailableProvider(message: string): Provider;
34
37
  export declare function createProviderInstance(options: ProviderInstanceOptions): Provider;
package/dist/provider.js CHANGED
@@ -9,6 +9,8 @@ import { createAnthropicMessagesProvider } from "./provider-anthropic.js";
9
9
  import { createArkResponsesProvider } 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
+ import { buildGrokSubscriptionHeaders, createGrokSubscriptionFetch, isGrokSubscriptionBaseUrl, } from "./provider-grok.js";
13
+ import { getChatGptFetch } from "./network/chatgpt-transport.js";
12
14
  import { createProviderProtocolArtifactFilter } from "./provider-artifacts.js";
13
15
  import { resolveProviderRequestConfig } from "./provider-transform.js";
14
16
  import { debugReasoningStream, summarizeDebugText } from "./reasoning-debug.js";
@@ -95,10 +97,22 @@ export function createProviderInstance(options) {
95
97
  auth: options.openAICodexAuth,
96
98
  });
97
99
  }
100
+ // Grok subscription rides the generic chat-completions path but needs the
101
+ // CLI identity headers the proxy gates on, plus a fetch that keeps the
102
+ // short-lived OAuth bearer fresh (and proxy-aware) across long sessions.
103
+ const grokSubscription = options.providerId === "grok" || isGrokSubscriptionBaseUrl(options.baseURL);
98
104
  const client = new OpenAI({
99
105
  apiKey: options.apiKey,
100
106
  baseURL: options.baseURL,
101
107
  timeout: resolveRequestTimeoutMs(process.env.BUBBLE_PROVIDER_REQUEST_TIMEOUT_MS),
108
+ ...(grokSubscription
109
+ ? {
110
+ defaultHeaders: buildGrokSubscriptionHeaders(),
111
+ fetch: (options.grokAuth
112
+ ? createGrokSubscriptionFetch(options.grokAuth)
113
+ : getChatGptFetch()),
114
+ }
115
+ : {}),
102
116
  });
103
117
  const fallbackModel = "gpt-4o";
104
118
  async function* streamChat(messages, chatOptions) {
@@ -1,5 +1,17 @@
1
1
  import type { AssistantMessage, Message, ThinkingLevel, Todo, ToolCall, ToolMessage, UserMessage } from "./types.js";
2
2
  import type { GoalState } from "./goal/store.js";
3
+ export interface ExternalRuntimeSessionMetadata {
4
+ /** External agent runtime that owns the conversational state. */
5
+ id: "grok";
6
+ /** Opaque ACP session identifier. Never contains credentials. */
7
+ sessionId?: string;
8
+ /** Server-selected model, when the runtime reports one. */
9
+ modelId?: string;
10
+ /** Reasoning effort selected for the external runtime. */
11
+ reasoningEffort?: ThinkingLevel;
12
+ /** Exact external runtime version used to create the session. */
13
+ version?: string;
14
+ }
3
15
  export interface SessionMetadata {
4
16
  model?: string;
5
17
  thinkingLevel?: ThinkingLevel;
@@ -12,8 +24,14 @@ export interface SessionMetadata {
12
24
  promptCacheKey?: string;
13
25
  /** Persisted autonomous goal (see src/goal). Survives /session resume. */
14
26
  goal?: GoalState;
27
+ /**
28
+ * External runtimes keep their own conversation state. Bubble persists only
29
+ * the opaque session binding and a UI transcript mirror; it never stores the
30
+ * runtime's OAuth credentials.
31
+ */
32
+ externalRuntime?: ExternalRuntimeSessionMetadata;
15
33
  }
16
- export type SessionMarkerKind = "model_switch" | "provider_switch" | "thinking_level_switch" | "skill_activated" | "mode_switch" | "conversation_clear";
34
+ export type SessionMarkerKind = "model_switch" | "provider_switch" | "thinking_level_switch" | "skill_activated" | "mode_switch" | "runtime_switch" | "conversation_clear";
17
35
  interface BaseSessionLogEntry {
18
36
  id: string;
19
37
  timestamp: number;