@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
@@ -1,3 +1,4 @@
1
+ import { normalizeModelToken } from "../agent/routing-catalog.js";
1
2
  import { wrapInSystemReminder } from "./reminders.js";
2
3
  /**
3
4
  * Delegation nudge for exploration-shaped tasks: injected at the decision
@@ -6,7 +7,7 @@ import { wrapInSystemReminder } from "./reminders.js";
6
7
  * keeps it away from ordinary implementation/debugging turns, so it cannot
7
8
  * amplify over-delegation.
8
9
  */
9
- const DELEGATION_NUDGE = "- If answering needs scanning many files and only the conclusion matters, delegate to a background subagent (spawn_agent); when it is the same read-only question over several independent items, fan out with agent_team.";
10
+ const DELEGATION_NUDGE = "- If answering needs scanning many files and only the conclusion matters, delegate to a background subagent (spawn_agent); when it is the same read-only question over several independent items, fan out with a run_workflow script.";
10
11
  export function reminderForTaskType(taskType, options = {}) {
11
12
  switch (taskType) {
12
13
  case "debugging":
@@ -62,3 +63,74 @@ Product discussion workflow:
62
63
  return undefined;
63
64
  }
64
65
  }
66
+ /**
67
+ * Deterministic detector for an explicit user request for a coordinated
68
+ * multi-agent run. Three rounds of prompt wording lost to the model's
69
+ * "agent team = parallel spawns" prior in live tests (opus-4.8, 2026-07-06);
70
+ * per the task-reminder principle above, a reminder injected at the decision
71
+ * turn is the lever that actually works — the harness remembers so the model
72
+ * does not have to.
73
+ */
74
+ /**
75
+ * User-named model resolution (model-routing design v3.6): when the user's
76
+ * message names a model that exists in the routable catalog, hand the main
77
+ * agent the EXACT provider:model id at the decision point. The catalog is a
78
+ * closed set, so matching is deterministic — the model must never retype an
79
+ * id from priors when the harness can resolve it ("gpt 5.6 sol" ->
80
+ * openai:gpt-5.6-sol; a Grok-4.5 parent invented "openai:gpt-5.6" without
81
+ * this, 2026-07-12 live case). Silent on no match: zero standing noise.
82
+ */
83
+ export function userNamedModelReminder(input, routableModels, canSpawn) {
84
+ if (!canSpawn || !routableModels || routableModels.length === 0)
85
+ return undefined;
86
+ const text = typeof input === "string"
87
+ ? input
88
+ : input.map((part) => ("text" in part ? String(part.text ?? "") : "")).join(" ");
89
+ if (!text.trim())
90
+ return undefined;
91
+ const normalizedInput = normalizeModelToken(text);
92
+ // Group by model id so multi-provider models (glm-5.2 on four providers)
93
+ // produce one line listing the routable providers.
94
+ const hitsByModel = new Map();
95
+ for (const entry of routableModels) {
96
+ const keys = [normalizeModelToken(entry.id), normalizeModelToken(entry.name)];
97
+ // Short keys ("o1", "glm") would match prose; require some substance.
98
+ if (!keys.some((key) => key.length >= 5 && normalizedInput.includes(key)))
99
+ continue;
100
+ const hit = hitsByModel.get(entry.id);
101
+ if (hit) {
102
+ if (!hit.providerIds.includes(entry.providerId))
103
+ hit.providerIds.push(entry.providerId);
104
+ }
105
+ else {
106
+ hitsByModel.set(entry.id, { entry, providerIds: [entry.providerId] });
107
+ }
108
+ }
109
+ if (hitsByModel.size === 0)
110
+ return undefined;
111
+ const lines = [...hitsByModel.values()].slice(0, 6).map(({ entry, providerIds }) => {
112
+ const primary = `${providerIds[0]}:${entry.id}`;
113
+ const alternates = providerIds.length > 1 ? ` (also on: ${providerIds.slice(1).join(", ")})` : "";
114
+ return `- "${entry.name}" → ${primary}${alternates}`;
115
+ });
116
+ return [
117
+ "The user's message names these configured models. Exact routable ids:",
118
+ ...lines,
119
+ "Use these exact ids in spawn_agent / run_workflow `model` params; do not retype model ids from memory.",
120
+ ].join("\n");
121
+ }
122
+ const ORCHESTRATION_REQUEST = /\b(?:workflows?|orchestrat\w*|agent[ -]?teams?|fan[ -]?out)\b|工作流|编排|(?:智能体|代理|agent)\s*(?:团队|小队)/i;
123
+ export function orchestrationRequestReminder(input, canRunWorkflow) {
124
+ if (!canRunWorkflow)
125
+ return undefined;
126
+ const text = typeof input === "string"
127
+ ? input
128
+ : input.map((part) => ("text" in part ? String(part.text ?? "") : "")).join(" ");
129
+ if (!ORCHESTRATION_REQUEST.test(text))
130
+ return undefined;
131
+ return [
132
+ "- This message explicitly asks for a coordinated multi-agent run (a workflow / orchestration / agent team).",
133
+ "Honor it with ONE run_workflow call whose script covers the whole fan-out.",
134
+ "Do not substitute parallel spawn_agent calls this turn — the user named the mechanism.",
135
+ ].join(" ");
136
+ }
@@ -138,14 +138,14 @@ function buildCallOptions(messages, chatOptions, providerOptions) {
138
138
  /**
139
139
  * Gemini 3 models take a graded thinking_level; 2.5-era models take a token
140
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.
141
+ * accept it (2.5 Flash); xhigh/max/ultra clamp to high.
142
142
  */
143
143
  function buildGoogleThinkingOptions(modelId, level) {
144
144
  if (!level)
145
145
  return undefined;
146
146
  if (level === "off")
147
147
  return { thinkingConfig: { thinkingBudget: 0 } };
148
- const clamped = level === "xhigh" || level === "max" ? "high" : level;
148
+ const clamped = level === "xhigh" || level === "max" || level === "ultra" ? "high" : level;
149
149
  if (modelId.includes("gemini-3")) {
150
150
  return { thinkingConfig: { thinkingLevel: clamped, includeThoughts: true } };
151
151
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Grok subscription provider support.
3
+ *
4
+ * The CLI chat proxy (cli-chat-proxy.grok.com) speaks the OpenAI
5
+ * chat-completions protocol, so the generic OpenAI provider does the heavy
6
+ * lifting. This module supplies the two Grok-specific pieces: the client
7
+ * identity headers the proxy gates on, and a fetch wrapper that keeps the
8
+ * OAuth bearer fresh across long sessions.
9
+ */
10
+ import type { OAuthCredentials } from "./oauth/types.js";
11
+ import { type ChatGptFetch } from "./network/chatgpt-transport.js";
12
+ export declare const GROK_SUBSCRIPTION_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
13
+ export declare const GROK_CLIENT_VERSION = "0.2.93";
14
+ export declare function isGrokSubscriptionBaseUrl(baseURL: string): boolean;
15
+ export declare function buildGrokSubscriptionHeaders(): Record<string, string>;
16
+ export interface GrokAuthAdapter {
17
+ getCredentials: () => OAuthCredentials | undefined | Promise<OAuthCredentials | undefined>;
18
+ refreshCredentials: (current?: OAuthCredentials) => Promise<OAuthCredentials>;
19
+ isExpired?: (credentials: OAuthCredentials, graceMs: number) => boolean;
20
+ }
21
+ /**
22
+ * Wrap a fetch so every request carries a fresh subscription bearer. The
23
+ * OpenAI client's static apiKey would go stale after ~6h; this reads (and
24
+ * proactively refreshes) the stored OAuth credentials per request instead.
25
+ */
26
+ export declare function createGrokSubscriptionFetch(auth: GrokAuthAdapter, baseFetch?: ChatGptFetch): ChatGptFetch;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Grok subscription provider support.
3
+ *
4
+ * The CLI chat proxy (cli-chat-proxy.grok.com) speaks the OpenAI
5
+ * chat-completions protocol, so the generic OpenAI provider does the heavy
6
+ * lifting. This module supplies the two Grok-specific pieces: the client
7
+ * identity headers the proxy gates on, and a fetch wrapper that keeps the
8
+ * OAuth bearer fresh across long sessions.
9
+ */
10
+ import { getChatGptFetch } from "./network/chatgpt-transport.js";
11
+ export const GROK_SUBSCRIPTION_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
12
+ // The proxy rejects requests below a minimum client version; identify as the
13
+ // same pinned CLI build Bubble verifies elsewhere.
14
+ export const GROK_CLIENT_VERSION = "0.2.93";
15
+ const TOKEN_REFRESH_GRACE_MS = 5 * 60 * 1000;
16
+ export function isGrokSubscriptionBaseUrl(baseURL) {
17
+ const normalized = baseURL.trim().replace(/\/+$/, "");
18
+ return normalized === GROK_SUBSCRIPTION_BASE_URL || normalized.startsWith(`${GROK_SUBSCRIPTION_BASE_URL}/`);
19
+ }
20
+ export function buildGrokSubscriptionHeaders() {
21
+ return {
22
+ "User-Agent": "grok-cli",
23
+ "x-grok-client-version": GROK_CLIENT_VERSION,
24
+ };
25
+ }
26
+ /**
27
+ * Wrap a fetch so every request carries a fresh subscription bearer. The
28
+ * OpenAI client's static apiKey would go stale after ~6h; this reads (and
29
+ * proactively refreshes) the stored OAuth credentials per request instead.
30
+ */
31
+ export function createGrokSubscriptionFetch(auth, baseFetch = getChatGptFetch()) {
32
+ let refreshPromise;
33
+ async function freshAccessToken() {
34
+ let credentials = await auth.getCredentials();
35
+ if (!credentials)
36
+ return undefined;
37
+ const expired = auth.isExpired
38
+ ? auth.isExpired(credentials, TOKEN_REFRESH_GRACE_MS)
39
+ : Date.now() >= credentials.expiresAt - TOKEN_REFRESH_GRACE_MS;
40
+ if ((expired || !credentials.accessToken) && credentials.refreshToken) {
41
+ if (!refreshPromise) {
42
+ refreshPromise = auth.refreshCredentials(credentials).finally(() => {
43
+ refreshPromise = undefined;
44
+ });
45
+ }
46
+ credentials = await refreshPromise;
47
+ }
48
+ return credentials.accessToken || undefined;
49
+ }
50
+ return async (input, init) => {
51
+ const token = await freshAccessToken();
52
+ if (!token) {
53
+ return baseFetch(input, init);
54
+ }
55
+ const headers = new Headers(init?.headers);
56
+ headers.set("Authorization", `Bearer ${token}`);
57
+ return baseFetch(input, { ...(init ?? {}), headers });
58
+ };
59
+ }
@@ -1,11 +1,14 @@
1
- import type { Provider, ReasoningEffort, ThinkingLevel, TokenUsage } from "./types.js";
1
+ import { type Provider, type ReasoningEffort, type ThinkingLevel, type TokenUsage } from "./types.js";
2
2
  import type { OAuthCredentials } from "./oauth/types.js";
3
3
  import { type ChatGptFetch } from "./network/chatgpt-transport.js";
4
4
  export interface CodexModelDescriptor {
5
5
  id: string;
6
6
  displayName?: string;
7
7
  contextWindow?: number;
8
+ useResponsesLite?: boolean;
8
9
  reasoningLevels?: ReasoningEffort[];
10
+ defaultReasoningLevel?: ReasoningEffort;
11
+ priority?: number;
9
12
  visibility?: string;
10
13
  minimalClientVersion?: string;
11
14
  /** Server-declared per-tool-output token cap (truncation_policy.limit when mode=tokens). */
@@ -39,4 +42,17 @@ export declare function fetchOpenAICodexModels(options: {
39
42
  accessToken: string;
40
43
  fetch?: ChatGptFetch;
41
44
  }): Promise<CodexModelDescriptor[]>;
45
+ export interface OpenAICodexModelCatalogResult {
46
+ descriptors: CodexModelDescriptor[];
47
+ status: "success" | "unavailable";
48
+ }
49
+ /**
50
+ * Fetches the account-scoped Codex catalog while preserving the distinction
51
+ * between an authoritative empty catalog and discovery being unavailable.
52
+ */
53
+ export declare function fetchOpenAICodexModelCatalog(options: {
54
+ baseURL: string;
55
+ accessToken: string;
56
+ fetch?: ChatGptFetch;
57
+ }): Promise<OpenAICodexModelCatalogResult>;
42
58
  export declare function sortCodexModelDescriptors(descriptors: CodexModelDescriptor[]): CodexModelDescriptor[];
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { listBuiltinModels } from "./model-catalog.js";
2
+ import { THINKING_LEVELS } from "./types.js";
3
+ import { getBuiltinModel, listBuiltinModels } from "./model-catalog.js";
3
4
  import { resolveProviderRequestConfig } from "./provider-transform.js";
4
5
  import { chatGptFetch } from "./network/chatgpt-transport.js";
5
6
  import { computeRetryDelayMs, getProviderMaxRetries, isRetryableHttpStatus, ProviderStreamInterruptedError, sleepBeforeRetry, } from "./network/retry.js";
@@ -68,6 +69,7 @@ export function createOpenAICodexProvider(options) {
68
69
  }
69
70
  async function* streamChat(messages, chatOptions) {
70
71
  const requestConfig = resolveProviderRequestConfig("openai-codex", chatOptions.model, chatOptions.thinkingLevel ?? options.thinkingLevel ?? "off");
72
+ const useResponsesLite = getBuiltinModel("openai-codex", chatOptions.model)?.useResponsesLite === true;
71
73
  const body = JSON.stringify(buildRequestBody(messages, {
72
74
  model: chatOptions.model,
73
75
  tools: chatOptions.tools,
@@ -76,6 +78,7 @@ export function createOpenAICodexProvider(options) {
76
78
  sessionId,
77
79
  providerId: options.providerId,
78
80
  promptCacheKey: options.promptCacheKey,
81
+ useResponsesLite,
79
82
  }));
80
83
  const sendRequest = async (forceRefresh = false) => {
81
84
  const { accessToken, accountId } = await resolveRequestAuth(forceRefresh);
@@ -85,6 +88,7 @@ export function createOpenAICodexProvider(options) {
85
88
  sessionId,
86
89
  signal: chatOptions.abortSignal,
87
90
  body,
91
+ useResponsesLite,
88
92
  }));
89
93
  };
90
94
  for (let attempt = 0;; attempt++) {
@@ -283,9 +287,17 @@ export function buildOpenAICodexPromptCacheKey(input) {
283
287
  .digest("hex");
284
288
  }
285
289
  export async function fetchOpenAICodexModels(options) {
290
+ const result = await fetchOpenAICodexModelCatalog(options);
291
+ return result.descriptors;
292
+ }
293
+ /**
294
+ * Fetches the account-scoped Codex catalog while preserving the distinction
295
+ * between an authoritative empty catalog and discovery being unavailable.
296
+ */
297
+ export async function fetchOpenAICodexModelCatalog(options) {
286
298
  const accountId = extractChatGptAccountId(options.accessToken);
287
299
  if (!accountId) {
288
- return [];
300
+ return { descriptors: [], status: "unavailable" };
289
301
  }
290
302
  const fetchImpl = options.fetch ?? chatGptFetch;
291
303
  for (const path of MODEL_DISCOVERY_PATHS) {
@@ -295,13 +307,17 @@ export async function fetchOpenAICodexModels(options) {
295
307
  }).catch(() => undefined);
296
308
  if (!response?.ok)
297
309
  continue;
298
- const payload = await response.json().catch(() => undefined);
299
- const descriptors = extractCodexModelDescriptors(payload);
300
- if (descriptors.length > 0) {
301
- return sortCodexModelDescriptors(descriptors);
302
- }
310
+ const parsed = await response.json()
311
+ .then((payload) => ({ ok: true, payload }))
312
+ .catch(() => ({ ok: false }));
313
+ if (!parsed.ok)
314
+ continue;
315
+ return {
316
+ descriptors: sortCodexModelDescriptors(extractCodexModelDescriptors(parsed.payload)),
317
+ status: "success",
318
+ };
303
319
  }
304
- return [];
320
+ return { descriptors: [], status: "unavailable" };
305
321
  }
306
322
  function buildRequestBody(messages, options) {
307
323
  const instructions = messages
@@ -309,11 +325,31 @@ function buildRequestBody(messages, options) {
309
325
  .map((message) => message.content)
310
326
  .join("\n\n");
311
327
  const input = messages.flatMap((message) => convertMessage(message));
328
+ const functionTools = (options.tools ?? []).map((tool) => ({
329
+ type: "function",
330
+ name: tool.name,
331
+ description: tool.description,
332
+ parameters: tool.parameters,
333
+ }));
334
+ const liteFunctionTools = options.toolChoice === "none" ? [] : functionTools;
335
+ if (options.useResponsesLite) {
336
+ input.unshift({
337
+ type: "additional_tools",
338
+ role: "developer",
339
+ tools: liteFunctionTools,
340
+ });
341
+ if (instructions) {
342
+ input.splice(1, 0, {
343
+ role: "developer",
344
+ content: [{ type: "input_text", text: instructions }],
345
+ });
346
+ }
347
+ }
312
348
  const body = {
313
349
  model: options.model,
314
350
  store: false,
315
351
  stream: true,
316
- instructions: instructions || undefined,
352
+ instructions: options.useResponsesLite ? "" : instructions || undefined,
317
353
  input,
318
354
  include: ["reasoning.encrypted_content"],
319
355
  prompt_cache_key: buildOpenAICodexPromptCacheKey({
@@ -321,17 +357,30 @@ function buildRequestBody(messages, options) {
321
357
  providerId: options.providerId,
322
358
  model: options.model,
323
359
  }),
324
- tool_choice: options.tools && options.tools.length > 0 ? options.toolChoice ?? "auto" : undefined,
325
- parallel_tool_calls: true,
360
+ tool_choice: options.useResponsesLite
361
+ ? "auto"
362
+ : options.tools && options.tools.length > 0 ? options.toolChoice ?? "auto" : undefined,
363
+ parallel_tool_calls: options.useResponsesLite ? false : true,
326
364
  text: { verbosity: "medium" },
365
+ client_metadata: options.useResponsesLite ? {
366
+ "x-codex-installation-id": options.sessionId,
367
+ session_id: options.sessionId,
368
+ thread_id: options.sessionId,
369
+ "x-codex-window-id": options.sessionId,
370
+ } : undefined,
327
371
  };
328
- if (options.tools && options.tools.length > 0) {
329
- body.tools = options.tools.map((tool) => ({
330
- type: "function",
331
- name: tool.name,
332
- description: tool.description,
333
- parameters: tool.parameters,
334
- }));
372
+ if (!options.useResponsesLite && functionTools.length > 0) {
373
+ body.tools = functionTools;
374
+ }
375
+ if (options.reasoningEffort && options.reasoningEffort !== "off") {
376
+ // Codex exposes Ultra as a client-level preset: it keeps the session/UI
377
+ // state at Ultra (which enables proactive delegation) but sends Max over
378
+ // the Responses wire protocol. Match the official Codex client mapping.
379
+ const wireEffort = options.reasoningEffort === "ultra" ? "max" : options.reasoningEffort;
380
+ body.reasoning = {
381
+ effort: wireEffort,
382
+ ...(options.useResponsesLite ? { context: "all_turns" } : {}),
383
+ };
335
384
  }
336
385
  return body;
337
386
  }
@@ -430,7 +479,7 @@ async function* parseSse(response) {
430
479
  function buildCodexRequestInit(options) {
431
480
  const init = {
432
481
  method: "POST",
433
- headers: buildSseHeaders(options.accessToken, options.accountId, options.sessionId),
482
+ headers: buildSseHeaders(options.accessToken, options.accountId, options.sessionId, options.useResponsesLite),
434
483
  signal: options.signal,
435
484
  body: options.body,
436
485
  keepalive: false,
@@ -505,17 +554,26 @@ function buildBaseHeaders(accessToken, accountId, sessionId, extraHeaders) {
505
554
  const headers = new Headers(extraHeaders);
506
555
  headers.set("Authorization", `Bearer ${accessToken}`);
507
556
  headers.set("ChatGPT-Account-Id", accountId);
508
- headers.set("originator", "bubble");
509
- headers.set("User-Agent", "bubble");
557
+ // The ChatGPT Codex backend gates newly listed models on the Codex client
558
+ // identity as well as client_version. Keep the official originator token and
559
+ // identify Bubble explicitly as a User-Agent suffix.
560
+ headers.set("originator", "codex_cli_rs");
561
+ headers.set("User-Agent", `codex_cli_rs/${CODEX_CLIENT_VERSION} (bubble)`);
510
562
  headers.set("session_id", sessionId);
511
563
  return headers;
512
564
  }
513
- function buildSseHeaders(accessToken, accountId, sessionId) {
565
+ function buildSseHeaders(accessToken, accountId, sessionId, useResponsesLite = false) {
514
566
  const headers = buildBaseHeaders(accessToken, accountId, sessionId, {
515
567
  accept: "text/event-stream",
516
568
  "content-type": "application/json",
517
569
  });
518
570
  headers.set("OpenAI-Beta", OPENAI_BETA_RESPONSES);
571
+ if (useResponsesLite) {
572
+ headers.set("x-openai-internal-codex-responses-lite", "true");
573
+ headers.set("thread_id", sessionId);
574
+ headers.set("x-codex-window-id", sessionId);
575
+ headers.set("x-codex-installation-id", sessionId);
576
+ }
519
577
  return headers;
520
578
  }
521
579
  function resolveCodexUrl(baseURL) {
@@ -530,9 +588,6 @@ function resolveRelativeUrl(baseURL, path) {
530
588
  const normalized = (baseURL.trim() || DEFAULT_CODEX_BASE_URL).replace(/\/+$/, "");
531
589
  return `${normalized}${path}`;
532
590
  }
533
- const REASONING_EFFORTS = [
534
- "off", "minimal", "low", "medium", "high", "xhigh", "max",
535
- ];
536
591
  function extractCodexModelDescriptors(payload) {
537
592
  const out = [];
538
593
  const seen = new Set();
@@ -553,22 +608,35 @@ function extractCodexModelDescriptors(payload) {
553
608
  const ctx = record.context_window;
554
609
  if (typeof ctx === "number" && ctx > 0)
555
610
  desc.contextWindow = ctx;
611
+ const useResponsesLite = record.use_responses_lite;
612
+ if (typeof useResponsesLite === "boolean")
613
+ desc.useResponsesLite = useResponsesLite;
556
614
  const visibility = record.visibility;
557
615
  if (typeof visibility === "string")
558
616
  desc.visibility = visibility;
559
617
  const minVer = record.minimal_client_version;
560
618
  if (typeof minVer === "string")
561
619
  desc.minimalClientVersion = minVer;
620
+ const priority = record.priority;
621
+ if (typeof priority === "number" && Number.isFinite(priority))
622
+ desc.priority = priority;
562
623
  const levels = record.supported_reasoning_levels;
563
624
  if (Array.isArray(levels)) {
564
- const efforts = new Set(["off"]);
625
+ const efforts = [];
565
626
  for (const level of levels) {
566
627
  const effort = level?.effort;
567
- if (typeof effort === "string" && REASONING_EFFORTS.includes(effort)) {
568
- efforts.add(effort);
628
+ if (typeof effort === "string"
629
+ && THINKING_LEVELS.includes(effort)
630
+ && !efforts.includes(effort)) {
631
+ efforts.push(effort);
569
632
  }
570
633
  }
571
- desc.reasoningLevels = REASONING_EFFORTS.filter((e) => efforts.has(e));
634
+ desc.reasoningLevels = efforts;
635
+ const defaultLevel = record.default_reasoning_level;
636
+ if (typeof defaultLevel === "string"
637
+ && efforts.includes(defaultLevel)) {
638
+ desc.defaultReasoningLevel = defaultLevel;
639
+ }
572
640
  }
573
641
  const truncPolicy = record.truncation_policy;
574
642
  if (truncPolicy && truncPolicy.mode === "tokens") {
@@ -617,6 +685,10 @@ export function sortCodexModelDescriptors(descriptors) {
617
685
  const rightFamily = parseCodexFamilyRank(right.id);
618
686
  if (leftFamily !== rightFamily)
619
687
  return rightFamily - leftFamily;
688
+ const leftPriority = left.priority ?? Number.MAX_SAFE_INTEGER;
689
+ const rightPriority = right.priority ?? Number.MAX_SAFE_INTEGER;
690
+ if (leftPriority !== rightPriority)
691
+ return leftPriority - rightPriority;
620
692
  const leftRank = preferred.get(left.id) ?? Number.MAX_SAFE_INTEGER;
621
693
  const rightRank = preferred.get(right.id) ?? Number.MAX_SAFE_INTEGER;
622
694
  if (leftRank !== rightRank)
@@ -9,6 +9,8 @@ import { type ProviderProtocol } from "./model-catalog.js";
9
9
  import { ModelConfig } from "./model-config.js";
10
10
  import { AuthStorage } from "./oauth/index.js";
11
11
  import { type OpenAICodexAuthAdapter } from "./provider-openai-codex.js";
12
+ import type { GrokAuthAdapter } from "./provider-grok.js";
13
+ import type { ThinkingLevel } from "./types.js";
12
14
  export interface ProviderProfile {
13
15
  id: string;
14
16
  name: string;
@@ -22,6 +24,37 @@ export interface ModelInfo {
22
24
  id: string;
23
25
  name: string;
24
26
  providerId: string;
27
+ reasoningLevels?: ThinkingLevel[];
28
+ defaultReasoningLevel?: ThinkingLevel;
29
+ contextWindow?: number;
30
+ useResponsesLite?: boolean;
31
+ toolOutputTokenLimit?: number;
32
+ /** Within-provider capability/cost tier (models.json / catalog annotation). */
33
+ tier?: import("./model-catalog.js").ModelTier;
34
+ /** Deterministic within-tier routing order (models.json only). */
35
+ routingPriority?: number;
36
+ }
37
+ export type ModelDiscoverySource = "remote" | "cache" | "static" | "fallback";
38
+ export interface ModelDiscoveryResult {
39
+ models: ModelInfo[];
40
+ source: ModelDiscoverySource;
41
+ /** True when `models` is the complete list for this provider/account. */
42
+ authoritative: boolean;
43
+ error?: string;
44
+ }
45
+ /**
46
+ * Read-only view of the discovery cache for routing consumers
47
+ * (docs/model-routing-design.md §1.4). `complete` is true only for a
48
+ * successful remote discovery that returned the provider's full catalog —
49
+ * static/fallback results are NEVER complete, regardless of the internal
50
+ * `authoritative` flag (which historically marks static fallbacks true).
51
+ */
52
+ export interface CachedDiscoverySnapshot {
53
+ models: ModelInfo[];
54
+ source: Exclude<ModelDiscoverySource, "cache">;
55
+ complete: boolean;
56
+ expiresAt: number;
57
+ identityKey: string;
25
58
  }
26
59
  export declare const BUILTIN_PROVIDERS: import("./model-catalog.js").BuiltinProviderDefinition[];
27
60
  export declare const USER_VISIBLE_PROVIDER_IDS: string[];
@@ -30,12 +63,32 @@ export declare class ProviderRegistry {
30
63
  private config;
31
64
  private modelConfig;
32
65
  private authStorage;
66
+ private modelDiscoveryCache;
67
+ private modelDiscoveryInFlight;
68
+ private modelDiscoveryGeneration;
69
+ /** Last membership seen per discovery key — survives cache eviction/TTL so
70
+ * a re-discovery with identical membership never bumps the revision. */
71
+ private lastDiscoveryMembership;
72
+ private routingRevision;
73
+ private identityFingerprints;
33
74
  constructor(config: UserConfig);
75
+ /**
76
+ * Monotonic counter of semantic routing-world changes: provider set,
77
+ * credential identity, provider key/config, discovery membership.
78
+ * Same-account OAuth token rotation deliberately does NOT bump it
79
+ * (docs/model-routing-design.md §1.6).
80
+ */
81
+ getRoutingRevision(): number;
82
+ private bumpRoutingRevision;
83
+ /** Stable identity for stored credentials: account id, else refresh-token hash. */
84
+ private credentialFingerprint;
85
+ private handleAuthMutation;
34
86
  getModelConfig(): ModelConfig;
35
87
  getAuthStorage(): AuthStorage;
36
88
  supportsOAuth(providerId: string): boolean;
37
89
  private resolveOAuthAuthKey;
38
90
  createOpenAICodexAuthAdapter(providerId: string): OpenAICodexAuthAdapter | undefined;
91
+ createGrokAuthAdapter(providerId: string): GrokAuthAdapter | undefined;
39
92
  getDefaultModel(providerId: string, authType?: ProviderProfile["authType"]): string | undefined;
40
93
  prepareProvider(providerId: string): Promise<void>;
41
94
  getConfigured(): ProviderProfile[];
@@ -46,6 +99,25 @@ export declare class ProviderRegistry {
46
99
  removeProvider(id: string): void;
47
100
  updateProviderKey(id: string, apiKey: string): void;
48
101
  listModels(provider: ProviderProfile): Promise<ModelInfo[]>;
102
+ discoverModels(provider: ProviderProfile, options?: {
103
+ forceRefresh?: boolean;
104
+ }): Promise<ModelDiscoveryResult>;
105
+ private performModelDiscovery;
106
+ private fallbackDiscovery;
107
+ private localModelsForProvider;
108
+ private discoveryIdentity;
109
+ private modelDiscoveryKey;
110
+ /**
111
+ * Read-only view of the cached discovery result for the provider's CURRENT
112
+ * configuration/identity (docs/model-routing-design.md §1.4). Returns
113
+ * undefined when nothing is cached or the cache has expired. `complete` is
114
+ * derived strictly: only a successful full remote catalog qualifies —
115
+ * never static or fallback results, whose internal `authoritative` flag
116
+ * this design does not trust.
117
+ */
118
+ getCachedDiscoverySnapshot(providerId: string): CachedDiscoverySnapshot | undefined;
119
+ private isCurrentModelDiscovery;
120
+ private applyDynamicDiscoveryMetadata;
49
121
  }
50
122
  /** Encode a model selection as "providerId:modelId". */
51
123
  export declare function encodeModel(providerId: string, modelId: string): string;