@bubblebrain-ai/bubble 0.0.58 → 0.0.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -6
- package/dist/agent/child-runner.js +5 -1
- package/dist/agent/internal-reminder-sanitizer.d.ts +1 -1
- package/dist/agent/internal-reminder-sanitizer.js +7 -1
- package/dist/agent/profiles.js +4 -4
- package/dist/agent/routing-catalog.d.ts +19 -1
- package/dist/agent/routing-catalog.js +32 -13
- package/dist/agent/subagent/runtime.d.ts +28 -0
- package/dist/agent/subagent/runtime.js +34 -30
- package/dist/agent/subagent-control.d.ts +7 -0
- package/dist/agent/subagent-control.js +2 -0
- package/dist/agent/subagent-lifecycle-reminder.js +2 -1
- package/dist/agent/subagent-wait-guidance.d.ts +2 -0
- package/dist/agent/subagent-wait-guidance.js +8 -0
- package/dist/agent/subtask-policy.js +4 -4
- package/dist/agent.d.ts +20 -11
- package/dist/agent.js +200 -117
- package/dist/anthropic-thinking.d.ts +4 -0
- package/dist/anthropic-thinking.js +11 -0
- package/dist/approval/controller.d.ts +20 -6
- package/dist/approval/controller.js +67 -8
- package/dist/approval/protected-paths.d.ts +1 -0
- package/dist/approval/protected-paths.js +35 -0
- package/dist/approval/session-cache.d.ts +12 -26
- package/dist/approval/session-cache.js +20 -50
- package/dist/approval/types.d.ts +27 -1
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +25 -2
- package/dist/config.d.ts +3 -0
- package/dist/config.js +7 -0
- package/dist/context/checkpoint.d.ts +18 -0
- package/dist/context/checkpoint.js +117 -0
- package/dist/context/compact-llm.js +7 -0
- package/dist/context/compact.d.ts +8 -0
- package/dist/context/compact.js +40 -7
- package/dist/context/llm-compactor.d.ts +36 -2
- package/dist/context/llm-compactor.js +227 -44
- package/dist/context/session-write-lock.d.ts +20 -0
- package/dist/context/session-write-lock.js +162 -0
- package/dist/context/tool-images.d.ts +3 -0
- package/dist/context/tool-images.js +35 -0
- package/dist/context/usage.js +22 -5
- package/dist/feishu/agent-host/run-driver.js +56 -9
- package/dist/lsp/service.d.ts +13 -1
- package/dist/lsp/service.js +163 -43
- package/dist/main.js +82 -16
- package/dist/mcp/config.d.ts +4 -0
- package/dist/mcp/config.js +17 -0
- package/dist/mcp/manager.d.ts +13 -1
- package/dist/mcp/manager.js +32 -1
- package/dist/mcp/transports.js +11 -0
- package/dist/memory/db.d.ts +1 -0
- package/dist/memory/db.js +20 -4
- package/dist/memory/phase1.js +122 -16
- package/dist/memory/prompts.js +3 -0
- package/dist/model-catalog.js +19 -3
- package/dist/model-config.d.ts +1 -0
- package/dist/model-config.js +5 -5
- package/dist/model-pricing.js +26 -9
- package/dist/network/provider-transport-log.d.ts +12 -0
- package/dist/network/provider-transport-log.js +32 -0
- package/dist/network/provider-transport.js +6 -1
- package/dist/network/retry.d.ts +1 -1
- package/dist/network/retry.js +1 -1
- package/dist/oauth/grok.d.ts +1 -0
- package/dist/oauth/grok.js +20 -16
- package/dist/oauth/openai-codex.d.ts +1 -0
- package/dist/oauth/openai-codex.js +29 -25
- package/dist/oauth/refresh-control.d.ts +12 -0
- package/dist/oauth/refresh-control.js +42 -0
- package/dist/oauth/storage.d.ts +55 -1
- package/dist/oauth/storage.js +281 -16
- package/dist/permissions/rule.d.ts +6 -3
- package/dist/permissions/rule.js +71 -12
- package/dist/permissions/settings.d.ts +28 -1
- package/dist/permissions/settings.js +93 -3
- package/dist/permissions/shell-command.d.ts +36 -0
- package/dist/permissions/shell-command.js +226 -0
- package/dist/permissions/trust-prompt.d.ts +10 -0
- package/dist/permissions/trust-prompt.js +34 -0
- package/dist/permissions/trust.d.ts +26 -0
- package/dist/permissions/trust.js +113 -0
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +24 -7
- package/dist/prompt/environment.js +2 -2
- package/dist/prompt/reminders.js +1 -1
- package/dist/prompt/runtime.js +1 -1
- package/dist/provider-anthropic.d.ts +6 -2
- package/dist/provider-anthropic.js +98 -18
- package/dist/provider-error-record.d.ts +9 -0
- package/dist/provider-error-record.js +42 -14
- package/dist/provider-grok.js +5 -1
- package/dist/provider-openai-codex.d.ts +8 -0
- package/dist/provider-openai-codex.js +107 -24
- package/dist/provider-registry.d.ts +45 -2
- package/dist/provider-registry.js +300 -88
- package/dist/provider-transform.js +8 -0
- package/dist/provider-zen-errors.d.ts +9 -0
- package/dist/provider-zen-errors.js +36 -0
- package/dist/provider.js +36 -5
- package/dist/sdk/index.d.ts +69 -8
- package/dist/sdk/index.js +252 -39
- package/dist/sdk/replay-event-log.d.ts +7 -0
- package/dist/sdk/replay-event-log.js +66 -16
- package/dist/session-context-fence.d.ts +16 -0
- package/dist/session-context-fence.js +26 -0
- package/dist/session-log.d.ts +11 -2
- package/dist/session-log.js +187 -39
- package/dist/session-types.d.ts +5 -1
- package/dist/session.d.ts +39 -7
- package/dist/session.js +341 -93
- package/dist/skills/format.d.ts +31 -1
- package/dist/skills/format.js +45 -24
- package/dist/slash-commands/commands.d.ts +3 -0
- package/dist/slash-commands/commands.js +29 -14
- package/dist/system-prompt.d.ts +3 -1
- package/dist/tasks/manager.d.ts +7 -0
- package/dist/tasks/manager.js +97 -18
- package/dist/tools/agent-lifecycle.js +11 -7
- package/dist/tools/child-tools.d.ts +1 -1
- package/dist/tools/child-tools.js +4 -4
- package/dist/tools/edit-apply.d.ts +0 -3
- package/dist/tools/edit-apply.js +45 -249
- package/dist/tools/edit.js +3 -3
- package/dist/tools/exit-plan-mode.d.ts +5 -0
- package/dist/tools/exit-plan-mode.js +3 -2
- package/dist/tools/file-state.d.ts +4 -0
- package/dist/tools/file-state.js +12 -3
- package/dist/tools/grep.js +20 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/ls.d.ts +3 -0
- package/dist/tools/ls.js +85 -0
- package/dist/tools/path-utils.d.ts +6 -0
- package/dist/tools/path-utils.js +38 -1
- package/dist/tools/sensitive-paths.d.ts +8 -0
- package/dist/tools/sensitive-paths.js +85 -3
- package/dist/tools/skill.d.ts +2 -0
- package/dist/tools/skill.js +34 -2
- package/dist/tools/task-tools.js +11 -7
- package/dist/tui/app.js +22 -12
- package/dist/tui/components/approval-dialog.d.ts +5 -5
- package/dist/tui/components/approval-dialog.js +16 -51
- package/dist/tui/components/skills-panel.d.ts +4 -0
- package/dist/tui/components/skills-panel.js +15 -1
- package/dist/tui/components/streaming-message.js +7 -2
- package/dist/tui/components/tasks-pane.d.ts +38 -0
- package/dist/tui/components/tasks-pane.js +182 -32
- package/dist/tui/components/transcript.d.ts +11 -4
- package/dist/tui/components/transcript.js +158 -71
- package/dist/tui/composer-autocomplete.d.ts +17 -2
- package/dist/tui/composer-autocomplete.js +79 -2
- package/dist/tui/controller/controller.d.ts +9 -4
- package/dist/tui/controller/controller.js +87 -95
- package/dist/tui/controller/goal-runtime-controller.js +1 -1
- package/dist/tui/controller/session-transition.js +1 -1
- package/dist/tui/model/subagent-view.d.ts +11 -19
- package/dist/tui/model/subagent-view.js +56 -85
- package/dist/tui/model/task-lifecycle.d.ts +57 -0
- package/dist/tui/model/task-lifecycle.js +124 -0
- package/dist/tui/model/trace-groups.js +10 -9
- package/dist/tui/trace-groups.js +10 -9
- package/dist/types.d.ts +20 -0
- package/dist/variant/variant-resolver.js +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts +1 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js +6 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts +2 -0
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts.map +1 -1
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js +8 -4
- package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js.map +1 -1
- package/package.json +11 -3
- package/dist/prompt/skills.d.ts +0 -2
- package/dist/prompt/skills.js +0 -4
- package/dist/tools/glob.d.ts +0 -5
- package/dist/tools/glob.js +0 -199
|
@@ -12,7 +12,7 @@ import { BUILTIN_PROVIDERS as CATALOG_PROVIDERS, clearDynamicModelMetadata, getB
|
|
|
12
12
|
import { ModelConfig } from "./model-config.js";
|
|
13
13
|
import { AuthStorage } from "./oauth/index.js";
|
|
14
14
|
import { fetchGeminiModels, geminiReasoningLevels } from "./provider-ai-sdk.js";
|
|
15
|
-
import { extractChatGptAccountId, fetchOpenAICodexModelCatalog } from "./provider-openai-codex.js";
|
|
15
|
+
import { extractChatGptAccountId, fetchOpenAICodexModelCatalog, getCodexClientVersion } from "./provider-openai-codex.js";
|
|
16
16
|
import { fetchGrokSubscriptionModels } from "./provider-grok.js";
|
|
17
17
|
import { refreshOpenAICodex } from "./oauth/openai-codex.js";
|
|
18
18
|
import { refreshGrok } from "./oauth/grok.js";
|
|
@@ -44,7 +44,9 @@ async function fetchOpenAICompatibleModelIds(provider) {
|
|
|
44
44
|
if (!response.ok)
|
|
45
45
|
throw new Error(`HTTP ${response.status}`);
|
|
46
46
|
const payload = (await response.json());
|
|
47
|
-
const entries = payload
|
|
47
|
+
const entries = payload?.data ?? payload?.models;
|
|
48
|
+
if (!Array.isArray(entries))
|
|
49
|
+
throw new Error("Model catalog response has no model list.");
|
|
48
50
|
return entries
|
|
49
51
|
.filter((entry) => typeof entry?.id === "string" && entry.id.trim().length > 0)
|
|
50
52
|
.map((entry) => ({
|
|
@@ -63,12 +65,17 @@ export function sanitizeProviderHeaders(value) {
|
|
|
63
65
|
}
|
|
64
66
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
65
67
|
}
|
|
68
|
+
function isCompleteDiscovery(result) {
|
|
69
|
+
return result.source === "remote" && result.authoritative && !result.error;
|
|
70
|
+
}
|
|
66
71
|
const MODEL_DISCOVERY_SUCCESS_TTL_MS = 60_000;
|
|
67
72
|
const MODEL_DISCOVERY_FAILURE_TTL_MS = 10_000;
|
|
68
73
|
/** How long a successful discovery survives on disk before a fresh fetch. */
|
|
69
74
|
const MODEL_DISCOVERY_DISK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
70
75
|
/** Discovery must never delay startup or a model picker for long. */
|
|
71
76
|
const MODEL_DISCOVERY_TIMEOUT_MS = 5_000;
|
|
77
|
+
// Per request: the ChatGPT backend is often reached through a user proxy.
|
|
78
|
+
const OPENAI_CODEX_MODEL_DISCOVERY_TIMEOUT_MS = 15_000;
|
|
72
79
|
// Included in the cache identity so a pre-single-model OpenRouter disk cache
|
|
73
80
|
// can never repopulate the picker with the old full remote catalog.
|
|
74
81
|
const OPENROUTER_CATALOG_SCOPE = "ox-alpha-only-v2";
|
|
@@ -123,6 +130,10 @@ export const USER_VISIBLE_PROVIDER_IDS = BUILTIN_PROVIDERS
|
|
|
123
130
|
export function isUserVisibleProvider(providerId) {
|
|
124
131
|
return USER_VISIBLE_PROVIDER_IDS.includes(providerId);
|
|
125
132
|
}
|
|
133
|
+
/** Judge expiry on the exact credential set the caller is about to send. */
|
|
134
|
+
function credentialsExpired(credentials, graceMs = 5 * 60 * 1000) {
|
|
135
|
+
return Date.now() >= credentials.expiresAt - graceMs;
|
|
136
|
+
}
|
|
126
137
|
export class ProviderRegistry {
|
|
127
138
|
config;
|
|
128
139
|
modelConfig;
|
|
@@ -165,10 +176,12 @@ export class ProviderRegistry {
|
|
|
165
176
|
const parsed = JSON.parse(readFileSync(this.discoveryDiskCachePath, "utf8"));
|
|
166
177
|
const now = Date.now();
|
|
167
178
|
for (const [key, entry] of Object.entries(parsed)) {
|
|
168
|
-
if (!entry || typeof entry.expiresAt !== "number"
|
|
179
|
+
if (!entry || typeof entry.expiresAt !== "number")
|
|
169
180
|
continue;
|
|
170
181
|
if (!entry.result || !Array.isArray(entry.result.models))
|
|
171
182
|
continue;
|
|
183
|
+
if (entry.expiresAt <= now && !(entry.providerId === "openai" && isCompleteDiscovery(entry.result)))
|
|
184
|
+
continue;
|
|
172
185
|
const provider = { id: entry.providerId ?? "", authType: entry.authType, protocol: entry.protocol };
|
|
173
186
|
// Capability fields may have been derived by an older Bubble build.
|
|
174
187
|
// Recompute them with the current code while retaining cached remote
|
|
@@ -181,6 +194,7 @@ export class ProviderRegistry {
|
|
|
181
194
|
providerId: provider.id,
|
|
182
195
|
authType: entry.authType,
|
|
183
196
|
protocol: entry.protocol,
|
|
197
|
+
confirmed: isCompleteDiscovery(result) ? { models: result.models, until: entry.expiresAt } : undefined,
|
|
184
198
|
});
|
|
185
199
|
// Rebuild the dynamic overlay so context window / reasoning levels
|
|
186
200
|
// resolve from the cached catalog at startup, not only on /model open.
|
|
@@ -199,17 +213,28 @@ export class ProviderRegistry {
|
|
|
199
213
|
return;
|
|
200
214
|
try {
|
|
201
215
|
const data = {};
|
|
216
|
+
const now = Date.now();
|
|
202
217
|
for (const [key, entry] of this.modelDiscoveryCache) {
|
|
203
|
-
|
|
204
|
-
continue;
|
|
205
|
-
data[key] = {
|
|
206
|
-
result: entry.result,
|
|
207
|
-
expiresAt: Date.now() + MODEL_DISCOVERY_DISK_TTL_MS,
|
|
218
|
+
const common = {
|
|
208
219
|
identityKey: entry.identityKey,
|
|
209
220
|
providerId: entry.providerId,
|
|
210
221
|
authType: entry.authType,
|
|
211
222
|
protocol: entry.protocol,
|
|
212
223
|
};
|
|
224
|
+
if (!entry.result.error) {
|
|
225
|
+
data[key] = { ...common, result: entry.result, expiresAt: now + MODEL_DISCOVERY_DISK_TTL_MS };
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
// A failed refresh must not erase the identity's last confirmed catalog
|
|
229
|
+
// from disk when an unrelated provider triggers a rewrite; persist the
|
|
230
|
+
// confirmation under its remaining horizon instead of dropping the key.
|
|
231
|
+
if (entry.confirmed && (entry.confirmed.until > now || entry.providerId === "openai")) {
|
|
232
|
+
data[key] = {
|
|
233
|
+
...common,
|
|
234
|
+
result: { models: entry.confirmed.models, source: "remote", authoritative: true },
|
|
235
|
+
expiresAt: entry.confirmed.until,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
213
238
|
}
|
|
214
239
|
mkdirSync(dirname(this.discoveryDiskCachePath), { recursive: true });
|
|
215
240
|
writeFileSync(this.discoveryDiskCachePath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
@@ -225,6 +250,10 @@ export class ProviderRegistry {
|
|
|
225
250
|
* (docs/model-routing-design.md §1.6).
|
|
226
251
|
*/
|
|
227
252
|
getRoutingRevision() {
|
|
253
|
+
// A login/logout in another Bubble process only reaches handleAuthMutation
|
|
254
|
+
// when the storage notices the file changed; callers cache against this
|
|
255
|
+
// number without touching credentials, so look now (one stat).
|
|
256
|
+
this.authStorage.sync();
|
|
228
257
|
return this.routingRevision;
|
|
229
258
|
}
|
|
230
259
|
bumpRoutingRevision() {
|
|
@@ -259,9 +288,57 @@ export class ProviderRegistry {
|
|
|
259
288
|
getAuthStorage() {
|
|
260
289
|
return this.authStorage;
|
|
261
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Run discovery for an account-scoped (OAuth) provider so the routing
|
|
293
|
+
* snapshot knows the account's real catalog before the routing prompt is
|
|
294
|
+
* composed and before the first subagent spawn. Discovery was previously
|
|
295
|
+
* triggered only by the model picker, which left tier routing on the static
|
|
296
|
+
* builtin list for the whole session unless the user opened /model.
|
|
297
|
+
* Resolves when discovery settles (it never throws: failures resolve to the
|
|
298
|
+
* local fallback); resolves immediately when a fresh cache exists or the
|
|
299
|
+
* provider is not account-scoped. Hosts bound the wait, see waitForModelDiscovery.
|
|
300
|
+
*/
|
|
301
|
+
warmModelDiscovery(providerId) {
|
|
302
|
+
const provider = this.getConfigured().find((item) => item.id === providerId);
|
|
303
|
+
if (!provider?.enabled || !provider.apiKey || provider.authType !== "oauth")
|
|
304
|
+
return Promise.resolve();
|
|
305
|
+
if (this.getCachedDiscoverySnapshot(providerId))
|
|
306
|
+
return Promise.resolve();
|
|
307
|
+
return this.discoverModels(provider).then(() => undefined, () => undefined);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Warm discovery but never hold startup hostage to the network: after
|
|
311
|
+
* `maxWaitMs` the host proceeds with whatever the snapshot has, and the
|
|
312
|
+
* still-running discovery updates the live accessor when it lands.
|
|
313
|
+
*/
|
|
314
|
+
async waitForModelDiscovery(providerId, maxWaitMs) {
|
|
315
|
+
let timer;
|
|
316
|
+
const deadline = new Promise((resolve) => {
|
|
317
|
+
timer = setTimeout(resolve, maxWaitMs);
|
|
318
|
+
timer.unref?.();
|
|
319
|
+
});
|
|
320
|
+
try {
|
|
321
|
+
await Promise.race([this.warmModelDiscovery(providerId), deadline]);
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
if (timer)
|
|
325
|
+
clearTimeout(timer);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
262
328
|
supportsOAuth(providerId) {
|
|
263
329
|
return !!getBuiltinProvider(providerId)?.supportsOAuth;
|
|
264
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* Auth-storage keys currently backing a provider's OAuth login. The visible
|
|
333
|
+
* `openai` provider may still be backed by the legacy `openai-codex` key,
|
|
334
|
+
* so UI state and logout must not check `has(providerId)` directly.
|
|
335
|
+
*/
|
|
336
|
+
getOAuthLoginKeys(providerId) {
|
|
337
|
+
const candidates = providerId === "openai" || providerId === "openai-codex"
|
|
338
|
+
? ["openai", "openai-codex"]
|
|
339
|
+
: [providerId];
|
|
340
|
+
return candidates.filter((key) => this.authStorage.has(key));
|
|
341
|
+
}
|
|
265
342
|
resolveOAuthAuthKey(providerId) {
|
|
266
343
|
if (providerId === "openai" || providerId === "openai-codex") {
|
|
267
344
|
if (this.authStorage.has("openai"))
|
|
@@ -280,30 +357,10 @@ export class ProviderRegistry {
|
|
|
280
357
|
let refreshPromise;
|
|
281
358
|
return {
|
|
282
359
|
getCredentials: readCredentials,
|
|
283
|
-
isExpired:
|
|
284
|
-
refreshCredentials: async () => {
|
|
360
|
+
isExpired: credentialsExpired,
|
|
361
|
+
refreshCredentials: async (used) => {
|
|
285
362
|
if (!refreshPromise) {
|
|
286
|
-
refreshPromise = (
|
|
287
|
-
const authKey = this.resolveOAuthAuthKey(providerId);
|
|
288
|
-
const current = this.authStorage.get(authKey);
|
|
289
|
-
if (!current?.refreshToken) {
|
|
290
|
-
throw new Error(`OpenAI OAuth credentials for ${providerId} are missing a refresh token.`);
|
|
291
|
-
}
|
|
292
|
-
const refreshed = await refreshOpenAICodex(current.refreshToken);
|
|
293
|
-
const next = {
|
|
294
|
-
type: "oauth",
|
|
295
|
-
accessToken: refreshed.accessToken,
|
|
296
|
-
refreshToken: refreshed.refreshToken,
|
|
297
|
-
expiresAt: refreshed.expiresAt,
|
|
298
|
-
idToken: refreshed.idToken || current.idToken,
|
|
299
|
-
accountId: refreshed.accountId || current.accountId,
|
|
300
|
-
};
|
|
301
|
-
this.authStorage.set("openai", next);
|
|
302
|
-
if (authKey !== "openai") {
|
|
303
|
-
this.authStorage.set(authKey, next);
|
|
304
|
-
}
|
|
305
|
-
return next;
|
|
306
|
-
})().finally(() => {
|
|
363
|
+
refreshPromise = this.refreshOpenAICredentials(providerId, used).finally(() => {
|
|
307
364
|
refreshPromise = undefined;
|
|
308
365
|
});
|
|
309
366
|
}
|
|
@@ -317,24 +374,10 @@ export class ProviderRegistry {
|
|
|
317
374
|
let refreshPromise;
|
|
318
375
|
return {
|
|
319
376
|
getCredentials: () => this.authStorage.get("grok"),
|
|
320
|
-
isExpired:
|
|
321
|
-
refreshCredentials: async () => {
|
|
377
|
+
isExpired: credentialsExpired,
|
|
378
|
+
refreshCredentials: async (used) => {
|
|
322
379
|
if (!refreshPromise) {
|
|
323
|
-
refreshPromise = (
|
|
324
|
-
const current = this.authStorage.get("grok");
|
|
325
|
-
if (!current?.refreshToken) {
|
|
326
|
-
throw new Error("Grok OAuth credentials are missing a refresh token. Run /login grok again.");
|
|
327
|
-
}
|
|
328
|
-
const refreshed = await refreshGrok(current.refreshToken);
|
|
329
|
-
const next = {
|
|
330
|
-
type: "oauth",
|
|
331
|
-
accessToken: refreshed.accessToken,
|
|
332
|
-
refreshToken: refreshed.refreshToken,
|
|
333
|
-
expiresAt: refreshed.expiresAt,
|
|
334
|
-
};
|
|
335
|
-
this.authStorage.set("grok", next);
|
|
336
|
-
return next;
|
|
337
|
-
})().finally(() => {
|
|
380
|
+
refreshPromise = this.refreshGrokCredentials(used).finally(() => {
|
|
338
381
|
refreshPromise = undefined;
|
|
339
382
|
});
|
|
340
383
|
}
|
|
@@ -353,38 +396,138 @@ export class ProviderRegistry {
|
|
|
353
396
|
return listBuiltinModels(providerId)[0]?.id;
|
|
354
397
|
}
|
|
355
398
|
async prepareProvider(providerId) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
399
|
+
// One snapshot decides both "is it expired" and "what did we hold": with
|
|
400
|
+
// two reads another process could rotate in between, and the fresh set
|
|
401
|
+
// would be taken for the held one and refreshed again for nothing.
|
|
402
|
+
if (providerId === "grok") {
|
|
403
|
+
const held = this.authStorage.get("grok");
|
|
404
|
+
if (held?.refreshToken && credentialsExpired(held))
|
|
405
|
+
await this.refreshGrokCredentials(held);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (providerId !== "openai" && providerId !== "openai-codex")
|
|
409
|
+
return;
|
|
410
|
+
const held = this.authStorage.get(this.resolveOAuthAuthKey(providerId));
|
|
411
|
+
if (held?.refreshToken && credentialsExpired(held))
|
|
412
|
+
await this.refreshOpenAICredentials(providerId, held);
|
|
413
|
+
}
|
|
414
|
+
refreshOpenAICredentials(providerId, used) {
|
|
415
|
+
return this.refreshSharedCredentials({
|
|
416
|
+
// Resolved under the lock: another process may have completed a fresh
|
|
417
|
+
// /login openai meanwhile, and a legacy openai-codex refresh must not be
|
|
418
|
+
// mirrored over it.
|
|
419
|
+
resolveKeys: () => {
|
|
420
|
+
const authKey = this.resolveOAuthAuthKey(providerId);
|
|
421
|
+
// "openai" is the canonical key; a legacy openai-codex login is mirrored.
|
|
422
|
+
return { authKey, writeKeys: authKey === "openai" ? ["openai"] : ["openai", authKey] };
|
|
423
|
+
},
|
|
424
|
+
used,
|
|
425
|
+
label: "OpenAI",
|
|
426
|
+
loginHint: "/login openai",
|
|
427
|
+
refresh: async (current) => {
|
|
428
|
+
const refreshed = await refreshOpenAICodex(current.refreshToken);
|
|
429
|
+
return {
|
|
361
430
|
type: "oauth",
|
|
362
431
|
accessToken: refreshed.accessToken,
|
|
363
432
|
refreshToken: refreshed.refreshToken,
|
|
364
433
|
expiresAt: refreshed.expiresAt,
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
434
|
+
idToken: refreshed.idToken || current.idToken,
|
|
435
|
+
accountId: refreshed.accountId || current.accountId,
|
|
436
|
+
};
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
refreshGrokCredentials(used) {
|
|
441
|
+
return this.refreshSharedCredentials({
|
|
442
|
+
resolveKeys: () => ({ authKey: "grok", writeKeys: ["grok"] }),
|
|
443
|
+
used,
|
|
444
|
+
label: "Grok",
|
|
445
|
+
loginHint: "/login grok",
|
|
446
|
+
refresh: async (current) => {
|
|
447
|
+
const refreshed = await refreshGrok(current.refreshToken);
|
|
448
|
+
return {
|
|
375
449
|
type: "oauth",
|
|
376
450
|
accessToken: refreshed.accessToken,
|
|
377
451
|
refreshToken: refreshed.refreshToken,
|
|
378
452
|
expiresAt: refreshed.expiresAt,
|
|
379
|
-
idToken: refreshed.idToken || creds.idToken,
|
|
380
|
-
accountId: refreshed.accountId || creds.accountId,
|
|
381
453
|
};
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* The one place a refresh token is spent. auth.json is shared by every
|
|
459
|
+
* Bubble process and refresh tokens are single-use, so the token is only
|
|
460
|
+
* presented under the cross-process lock and only after re-reading the file:
|
|
461
|
+
* if another process already rotated it, its result is adopted and no
|
|
462
|
+
* request is made. A rejected token gets the same second look before the
|
|
463
|
+
* error surfaces, because the rotation may have landed mid-request.
|
|
464
|
+
*
|
|
465
|
+
* `used` is the credential set the caller was holding when it asked (the
|
|
466
|
+
* provider passes it, also for a forced refresh after a 401): "rotated" means
|
|
467
|
+
* the file no longer holds that set, not merely that it is unexpired.
|
|
468
|
+
*/
|
|
469
|
+
refreshSharedCredentials(options) {
|
|
470
|
+
const { label, loginHint, refresh } = options;
|
|
471
|
+
const held = options.used ?? this.authStorage.get(options.resolveKeys().authKey);
|
|
472
|
+
return this.authStorage.withRefreshLock(async () => {
|
|
473
|
+
this.authStorage.reload();
|
|
474
|
+
const { authKey, writeKeys } = options.resolveKeys();
|
|
475
|
+
const current = this.authStorage.get(authKey);
|
|
476
|
+
if (!current?.refreshToken) {
|
|
477
|
+
throw new Error(`${label} OAuth credentials are missing a refresh token. Run ${loginHint} again.`);
|
|
478
|
+
}
|
|
479
|
+
const rotatedElsewhere = (since) => {
|
|
480
|
+
this.authStorage.reload();
|
|
481
|
+
// Re-resolved every time: a login elsewhere may have created the
|
|
482
|
+
// canonical key since, and it is then the entry to adopt.
|
|
483
|
+
const latest = this.authStorage.get(options.resolveKeys().authKey);
|
|
484
|
+
if (!latest || !since)
|
|
485
|
+
return undefined;
|
|
486
|
+
if (latest.refreshToken === since.refreshToken && latest.accessToken === since.accessToken)
|
|
487
|
+
return undefined;
|
|
488
|
+
return credentialsExpired(latest) ? undefined : latest;
|
|
489
|
+
};
|
|
490
|
+
const adopted = rotatedElsewhere(held);
|
|
491
|
+
if (adopted)
|
|
492
|
+
return adopted;
|
|
493
|
+
let next;
|
|
494
|
+
try {
|
|
495
|
+
next = await refresh(current);
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
const rescued = rotatedElsewhere(current);
|
|
499
|
+
if (rescued)
|
|
500
|
+
return rescued;
|
|
501
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
502
|
+
// A 400/401 from the token endpoint is a verdict on the refresh token
|
|
503
|
+
// (reused, revoked, expired), not a transient failure: say what to do.
|
|
504
|
+
if (/Token refresh failed: 40[01]\b|refresh_token_reused|invalid_grant/.test(message)) {
|
|
505
|
+
throw new Error(`${label} login is no longer valid. Run ${loginHint} to sign in again.\n${message}`);
|
|
385
506
|
}
|
|
507
|
+
throw error;
|
|
386
508
|
}
|
|
387
|
-
|
|
509
|
+
// A /login or /logout elsewhere may have landed while the request was in
|
|
510
|
+
// flight (they do not take the refresh lock): their outcome wins.
|
|
511
|
+
if (!this.authStorage.replaceIfUnchanged(authKey, next, current.refreshToken)) {
|
|
512
|
+
const replaced = this.authStorage.get(authKey);
|
|
513
|
+
if (replaced)
|
|
514
|
+
return replaced;
|
|
515
|
+
throw new Error(`${label} credentials were removed while refreshing. Run ${loginHint} to sign in again.`);
|
|
516
|
+
}
|
|
517
|
+
for (const key of writeKeys) {
|
|
518
|
+
if (key === authKey)
|
|
519
|
+
continue;
|
|
520
|
+
// Mirror keys were absent when resolved under the lock. If a login
|
|
521
|
+
// created one since, it is now the preferred entry: leave it alone and
|
|
522
|
+
// hand its credentials to the caller instead of the old account's.
|
|
523
|
+
if (!this.authStorage.replaceIfUnchanged(key, next, null)) {
|
|
524
|
+
const preferred = this.authStorage.get(options.resolveKeys().authKey);
|
|
525
|
+
if (preferred && !credentialsExpired(preferred))
|
|
526
|
+
return preferred;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return next;
|
|
530
|
+
});
|
|
388
531
|
}
|
|
389
532
|
getConfigured() {
|
|
390
533
|
// 1. Try models.json first
|
|
@@ -459,6 +602,14 @@ export class ProviderRegistry {
|
|
|
459
602
|
});
|
|
460
603
|
}
|
|
461
604
|
}
|
|
605
|
+
// OAuth providers can be injected even when models.json owns the model
|
|
606
|
+
// catalog. Honor the separately stored enable preference in either path.
|
|
607
|
+
const enablePreferences = new Map(this.config.getProviders().map((p) => [p.id, p.enabled]));
|
|
608
|
+
for (const provider of providers) {
|
|
609
|
+
if (provider.authType === "oauth" && enablePreferences.has(provider.id)) {
|
|
610
|
+
provider.enabled = enablePreferences.get(provider.id) !== false && !!provider.apiKey;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
462
613
|
return providers;
|
|
463
614
|
}
|
|
464
615
|
getEnabled() {
|
|
@@ -515,6 +666,8 @@ export class ProviderRegistry {
|
|
|
515
666
|
async discoverModels(provider, options = {}) {
|
|
516
667
|
const key = this.modelDiscoveryKey(provider);
|
|
517
668
|
const now = Date.now();
|
|
669
|
+
// A failed refresh must carry the last confirmed catalog forward.
|
|
670
|
+
const retainedConfirmation = this.modelDiscoveryCache.get(key)?.confirmed;
|
|
518
671
|
if (!options.forceRefresh) {
|
|
519
672
|
const cached = this.modelDiscoveryCache.get(key);
|
|
520
673
|
if (cached && cached.expiresAt > now) {
|
|
@@ -527,11 +680,13 @@ export class ProviderRegistry {
|
|
|
527
680
|
// without waiting up to 24 hours or requiring a manual Ctrl+R refresh.
|
|
528
681
|
const local = this.localModelsForProvider(provider);
|
|
529
682
|
const localIds = new Set(local.map((model) => model.id));
|
|
530
|
-
const models =
|
|
531
|
-
?
|
|
532
|
-
: cachedResult.
|
|
533
|
-
?
|
|
534
|
-
:
|
|
683
|
+
const models = provider.id === "openai"
|
|
684
|
+
? cachedResult.models
|
|
685
|
+
: cachedResult.source === "static"
|
|
686
|
+
? local
|
|
687
|
+
: cachedResult.authoritative
|
|
688
|
+
? cachedResult.models
|
|
689
|
+
: [...local, ...cachedResult.models.filter((model) => !localIds.has(model.id))];
|
|
535
690
|
const result = { ...cachedResult, models, source: "cache" };
|
|
536
691
|
const current = this.getConfigured().find((item) => item.id === provider.id);
|
|
537
692
|
if (!current || this.modelDiscoveryKey(current) === key) {
|
|
@@ -544,7 +699,8 @@ export class ProviderRegistry {
|
|
|
544
699
|
return inFlight;
|
|
545
700
|
}
|
|
546
701
|
else {
|
|
547
|
-
|
|
702
|
+
// Keep the last confirmation readable while the replacement is in flight.
|
|
703
|
+
// Forced discovery bypasses freshness without invalidating a good catalog.
|
|
548
704
|
// Do not let a superseded request's finally-handler remove this retry.
|
|
549
705
|
this.modelDiscoveryInFlight.delete(key);
|
|
550
706
|
}
|
|
@@ -552,10 +708,12 @@ export class ProviderRegistry {
|
|
|
552
708
|
this.modelDiscoveryGeneration.set(provider.id, generation);
|
|
553
709
|
let pending;
|
|
554
710
|
pending = this.performModelDiscovery(provider).then((rawResult) => {
|
|
555
|
-
const result = normalizeProviderDiscoveryMetadata(provider, rawResult
|
|
711
|
+
const result = normalizeProviderDiscoveryMetadata(provider, provider.id === "openai" && rawResult.error
|
|
712
|
+
? { ...rawResult, models: retainedConfirmation?.models ?? [] }
|
|
713
|
+
: rawResult);
|
|
556
714
|
if (!this.isCurrentModelDiscovery(provider, key, generation)) {
|
|
557
715
|
return {
|
|
558
|
-
models: this.localModelsForProvider(this.getConfigured().find((item) => item.id === provider.id) ?? provider),
|
|
716
|
+
models: provider.id === "openai" ? [] : this.localModelsForProvider(this.getConfigured().find((item) => item.id === provider.id) ?? provider),
|
|
559
717
|
source: "fallback",
|
|
560
718
|
authoritative: false,
|
|
561
719
|
error: "Model catalog changed while it was refreshing.",
|
|
@@ -563,9 +721,15 @@ export class ProviderRegistry {
|
|
|
563
721
|
}
|
|
564
722
|
this.applyDynamicDiscoveryMetadata(provider, result);
|
|
565
723
|
const previous = this.lastDiscoveryMembership.get(key);
|
|
724
|
+
const stored = { ...result, source: result.source === "cache" ? "remote" : result.source };
|
|
566
725
|
this.modelDiscoveryCache.set(key, {
|
|
567
|
-
result:
|
|
726
|
+
result: stored,
|
|
568
727
|
expiresAt: Date.now() + (result.error ? MODEL_DISCOVERY_FAILURE_TTL_MS : MODEL_DISCOVERY_SUCCESS_TTL_MS),
|
|
728
|
+
// A complete success starts a new confirmation horizon; anything else
|
|
729
|
+
// (failure fallback, partial union) carries the previous one forward.
|
|
730
|
+
confirmed: isCompleteDiscovery(stored)
|
|
731
|
+
? { models: stored.models, until: Date.now() + MODEL_DISCOVERY_DISK_TTL_MS }
|
|
732
|
+
: this.modelDiscoveryCache.get(key)?.confirmed ?? retainedConfirmation,
|
|
569
733
|
identityKey: this.discoveryIdentity(provider),
|
|
570
734
|
providerId: provider.id,
|
|
571
735
|
authType: provider.authType,
|
|
@@ -591,7 +755,7 @@ export class ProviderRegistry {
|
|
|
591
755
|
}
|
|
592
756
|
async performModelDiscovery(provider) {
|
|
593
757
|
const customModels = filterProviderModels(provider.id, this.modelConfig.getCustomModels(provider.id));
|
|
594
|
-
if (customModels.length > 0) {
|
|
758
|
+
if (customModels.length > 0 && provider.id !== "openai") {
|
|
595
759
|
return { models: customModels, source: "static", authoritative: true };
|
|
596
760
|
}
|
|
597
761
|
if (provider.id === "openrouter") {
|
|
@@ -659,9 +823,10 @@ export class ProviderRegistry {
|
|
|
659
823
|
const catalog = await fetchOpenAICodexModelCatalog({
|
|
660
824
|
baseURL: currentProvider.baseURL,
|
|
661
825
|
accessToken: currentProvider.apiKey,
|
|
826
|
+
timeoutMs: OPENAI_CODEX_MODEL_DISCOVERY_TIMEOUT_MS,
|
|
662
827
|
});
|
|
663
828
|
if (catalog.status === "unavailable") {
|
|
664
|
-
throw new Error(
|
|
829
|
+
throw new Error(`OpenAI Codex model catalog is unavailable${catalog.reason ? ` (${catalog.reason})` : ""}.`);
|
|
665
830
|
}
|
|
666
831
|
const visible = catalog.descriptors.filter((descriptor) => descriptor.visibility !== "hide");
|
|
667
832
|
return {
|
|
@@ -749,10 +914,19 @@ export class ProviderRegistry {
|
|
|
749
914
|
// while unlisted), remote-only ids are appended, obvious non-chat
|
|
750
915
|
// modalities are filtered out. Not authoritative — membership is a union,
|
|
751
916
|
// so nothing downstream may treat it as a closed allowlist.
|
|
752
|
-
if (isOpenAICompatibleProtocol(provider.protocol) && provider.apiKey) {
|
|
917
|
+
if ((isOpenAICompatibleProtocol(provider.protocol) || provider.id === "openai") && provider.apiKey) {
|
|
753
918
|
try {
|
|
754
919
|
const remote = await fetchOpenAICompatibleModelIds(provider);
|
|
755
920
|
const local = this.localModelsForProvider(provider);
|
|
921
|
+
if (provider.id === "openai") {
|
|
922
|
+
const metadata = new Map(local.map(model => [model.id, model]));
|
|
923
|
+
return {
|
|
924
|
+
models: remote.filter(entry => isLikelyChatModelId(entry.id)).map(entry => ({
|
|
925
|
+
...metadata.get(entry.id), id: entry.id, name: entry.name || entry.id, providerId: provider.id,
|
|
926
|
+
})),
|
|
927
|
+
source: "remote", authoritative: true,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
756
930
|
const known = new Set(local.map((model) => model.id));
|
|
757
931
|
const extras = remote
|
|
758
932
|
.filter((entry) => !known.has(entry.id) && isLikelyChatModelId(entry.id))
|
|
@@ -767,21 +941,23 @@ export class ProviderRegistry {
|
|
|
767
941
|
}
|
|
768
942
|
return { models: [...local, ...extras], source: "remote", authoritative: false };
|
|
769
943
|
}
|
|
770
|
-
catch {
|
|
944
|
+
catch (error) {
|
|
945
|
+
if (provider.id === "openai")
|
|
946
|
+
return this.fallbackDiscovery(provider, error);
|
|
771
947
|
// Vendors that don't implement /models (or gate it) fall back silently:
|
|
772
948
|
// the curated catalog is the contract, discovery is a bonus.
|
|
773
949
|
return { models: this.localModelsForProvider(provider), source: "static", authoritative: true };
|
|
774
950
|
}
|
|
775
951
|
}
|
|
776
952
|
return {
|
|
777
|
-
models: this.localModelsForProvider(provider),
|
|
953
|
+
models: provider.id === "openai" ? [] : this.localModelsForProvider(provider),
|
|
778
954
|
source: "static",
|
|
779
955
|
authoritative: true,
|
|
780
956
|
};
|
|
781
957
|
}
|
|
782
958
|
fallbackDiscovery(provider, error) {
|
|
783
959
|
return {
|
|
784
|
-
models: this.localModelsForProvider(provider),
|
|
960
|
+
models: provider.id === "openai" ? [] : this.localModelsForProvider(provider),
|
|
785
961
|
source: "fallback",
|
|
786
962
|
authoritative: false,
|
|
787
963
|
error: modelDiscoveryError(error),
|
|
@@ -825,7 +1001,15 @@ export class ProviderRegistry {
|
|
|
825
1001
|
provider.authType ?? "api",
|
|
826
1002
|
provider.protocol ?? "default",
|
|
827
1003
|
this.discoveryIdentity(provider),
|
|
828
|
-
provider.id === "openrouter"
|
|
1004
|
+
provider.id === "openrouter"
|
|
1005
|
+
? OPENROUTER_CATALOG_SCOPE
|
|
1006
|
+
// The ChatGPT backend filters /codex/models by the client version we
|
|
1007
|
+
// claim, so a catalog fetched under an older pin (possibly by another
|
|
1008
|
+
// still-running Bubble sharing the disk cache) must not satisfy this
|
|
1009
|
+
// build's discovery.
|
|
1010
|
+
: provider.id === "openai"
|
|
1011
|
+
? `remote-only-v1:${provider.authType === "oauth" ? `codex-client:${getCodexClientVersion()}` : "api"}`
|
|
1012
|
+
: undefined,
|
|
829
1013
|
]);
|
|
830
1014
|
}
|
|
831
1015
|
/**
|
|
@@ -835,18 +1019,46 @@ export class ProviderRegistry {
|
|
|
835
1019
|
* derived strictly: only a successful full remote catalog qualifies —
|
|
836
1020
|
* never static or fallback results, whose internal `authoritative` flag
|
|
837
1021
|
* this design does not trust.
|
|
1022
|
+
* Desktop catalogs can explicitly retain the last confirmation beyond its
|
|
1023
|
+
* routing horizon; ordinary routing callers keep the default expiry policy.
|
|
838
1024
|
*/
|
|
839
|
-
getCachedDiscoverySnapshot(providerId) {
|
|
1025
|
+
getCachedDiscoverySnapshot(providerId, options = {}) {
|
|
840
1026
|
const provider = this.getConfigured().find((item) => item.id === providerId);
|
|
841
1027
|
if (!provider)
|
|
842
1028
|
return undefined;
|
|
843
1029
|
const cached = this.modelDiscoveryCache.get(this.modelDiscoveryKey(provider));
|
|
844
|
-
if (!cached
|
|
1030
|
+
if (!cached)
|
|
1031
|
+
return undefined;
|
|
1032
|
+
const now = Date.now();
|
|
1033
|
+
const fresh = cached.expiresAt > now;
|
|
1034
|
+
if (fresh && isCompleteDiscovery(cached.result)) {
|
|
1035
|
+
return {
|
|
1036
|
+
models: cached.result.models,
|
|
1037
|
+
source: cached.result.source,
|
|
1038
|
+
complete: true,
|
|
1039
|
+
expiresAt: cached.expiresAt,
|
|
1040
|
+
identityKey: cached.identityKey,
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
// The live result is stale or not complete: routing keeps the last
|
|
1044
|
+
// confirmed catalog for this identity until its horizon passes, so a
|
|
1045
|
+
// long session does not lose its tier candidates after the freshness
|
|
1046
|
+
// minute or across one failed refresh.
|
|
1047
|
+
if (cached.confirmed && (cached.confirmed.until > now || options.allowExpiredConfirmation)) {
|
|
1048
|
+
return {
|
|
1049
|
+
models: cached.confirmed.models,
|
|
1050
|
+
source: "cache",
|
|
1051
|
+
complete: true,
|
|
1052
|
+
expiresAt: cached.confirmed.until,
|
|
1053
|
+
identityKey: cached.identityKey,
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
if (!fresh)
|
|
845
1057
|
return undefined;
|
|
846
1058
|
return {
|
|
847
1059
|
models: cached.result.models,
|
|
848
1060
|
source: cached.result.source,
|
|
849
|
-
complete:
|
|
1061
|
+
complete: false,
|
|
850
1062
|
expiresAt: cached.expiresAt,
|
|
851
1063
|
identityKey: cached.identityKey,
|
|
852
1064
|
};
|
|
@@ -31,6 +31,14 @@ function isFireworksKimi(providerId, modelId) {
|
|
|
31
31
|
export function resolveProviderRequestConfig(providerId, modelId, requestedLevel) {
|
|
32
32
|
const supportedLevels = getAvailableThinkingLevels(providerId, modelId);
|
|
33
33
|
const effectiveThinkingLevel = normalizeThinkingLevel(requestedLevel, supportedLevels);
|
|
34
|
+
if (providerId === "opencode-zen" && modelId === "space-bunny-free") {
|
|
35
|
+
const level = normalizeInheritedThinkingLevel(providerId, modelId, requestedLevel);
|
|
36
|
+
return {
|
|
37
|
+
effectiveThinkingLevel: level,
|
|
38
|
+
reasoningContentEcho: "tool_calls",
|
|
39
|
+
extraBody: { reasoning_effort: level },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
34
42
|
if (providerId === "openai-codex") {
|
|
35
43
|
const model = getBuiltinModel(providerId, modelId);
|
|
36
44
|
if (!model || model.reasoningLevels.length === 0) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ProviderStreamInterruptedError } from "./network/retry.js";
|
|
2
|
+
/** Only Zen's opaque upstream rejection is recoverable, not arbitrary 422s. */
|
|
3
|
+
export declare function isSpaceBunnyUpstreamRejection(providerId: string | undefined, model: string, error: unknown): boolean;
|
|
4
|
+
export declare function recoverableSpaceBunnyError(error: unknown): ProviderStreamInterruptedError;
|
|
5
|
+
/** Counts only: never persist prompts, tool arguments, URLs or API headers. */
|
|
6
|
+
export declare function chatRequestShape(body: {
|
|
7
|
+
messages?: Record<string, unknown>[];
|
|
8
|
+
tools?: unknown[];
|
|
9
|
+
}): Record<string, number>;
|