@bubblebrain-ai/bubble 0.0.13 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/execution-governor.js +1 -1
- package/dist/agent/tool-intent.js +1 -0
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +589 -316
- package/dist/approval/controller.d.ts +1 -0
- package/dist/approval/controller.js +20 -3
- package/dist/approval/tool-helper.js +2 -0
- package/dist/approval/types.d.ts +14 -1
- package/dist/context/compact.js +9 -3
- package/dist/context/projector.js +27 -12
- package/dist/debug-trace.d.ts +27 -0
- package/dist/debug-trace.js +385 -0
- package/dist/feishu/agent-host/approval-card.js +9 -0
- package/dist/feishu/serve.js +7 -1
- package/dist/main.js +28 -0
- package/dist/model-catalog.js +1 -0
- package/dist/orchestrator/default-hooks.js +19 -8
- package/dist/orchestrator/hooks.d.ts +1 -0
- package/dist/prompt/environment.js +2 -0
- package/dist/prompt/reminders.d.ts +5 -6
- package/dist/prompt/reminders.js +8 -9
- package/dist/prompt/runtime.js +2 -2
- package/dist/provider-openai-codex.d.ts +7 -0
- package/dist/provider-openai-codex.js +265 -124
- package/dist/provider-registry.d.ts +2 -0
- package/dist/provider-registry.js +58 -9
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +5 -1
- package/dist/session-log.js +13 -1
- package/dist/slash-commands/commands.js +12 -0
- package/dist/slash-commands/types.d.ts +2 -0
- package/dist/stats/usage.d.ts +52 -0
- package/dist/stats/usage.js +414 -0
- package/dist/tools/apply-patch.d.ts +9 -0
- package/dist/tools/apply-patch.js +330 -0
- package/dist/tools/bash.js +205 -44
- package/dist/tools/edit-apply.d.ts +5 -2
- package/dist/tools/edit-apply.js +221 -31
- package/dist/tools/edit.js +12 -3
- package/dist/tools/file-mutation-queue.d.ts +1 -0
- package/dist/tools/file-mutation-queue.js +12 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +7 -1
- package/dist/tools/patch-apply.d.ts +41 -0
- package/dist/tools/patch-apply.js +312 -0
- package/dist/tools/server-manager.d.ts +36 -0
- package/dist/tools/server-manager.js +234 -0
- package/dist/tools/server.d.ts +6 -0
- package/dist/tools/server.js +245 -0
- package/dist/tools/write.d.ts +3 -6
- package/dist/tools/write.js +26 -46
- package/dist/tui/display-history.d.ts +1 -0
- package/dist/tui/display-history.js +5 -4
- package/dist/tui/edit-diff.js +6 -1
- package/dist/tui/model-picker-data.d.ts +10 -0
- package/dist/tui/model-picker-data.js +32 -0
- package/dist/tui/run.js +632 -89
- package/dist/tui/tool-renderers/fallback.js +1 -1
- package/dist/tui/tool-renderers/write-preview.js +2 -0
- package/dist/tui/trace-groups.js +10 -3
- package/dist/tui-ink/app.js +1 -4
- package/dist/tui-ink/approval/approval-dialog.js +7 -1
- package/dist/tui-ink/display-history.d.ts +1 -0
- package/dist/tui-ink/display-history.js +5 -4
- package/dist/tui-ink/message-list.js +14 -8
- package/dist/tui-ink/trace-groups.js +1 -1
- package/dist/tui-opentui/app.js +2 -0
- package/dist/tui-opentui/approval/approval-dialog.js +7 -1
- package/dist/tui-opentui/display-history.d.ts +1 -0
- package/dist/tui-opentui/display-history.js +5 -4
- package/dist/tui-opentui/edit-diff.js +6 -1
- package/dist/tui-opentui/message-list.js +6 -3
- package/dist/tui-opentui/trace-groups.js +10 -3
- package/dist/types.d.ts +12 -2
- package/package.json +1 -1
|
@@ -3,6 +3,9 @@ import { listBuiltinModels } from "./model-catalog.js";
|
|
|
3
3
|
import { resolveProviderRequestConfig } from "./provider-transform.js";
|
|
4
4
|
const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
5
5
|
const OPENAI_BETA_RESPONSES = "responses=experimental";
|
|
6
|
+
const TOKEN_REFRESH_GRACE_MS = 5 * 60 * 1000;
|
|
7
|
+
const CODEX_TRANSPORT_MAX_RETRIES = 2;
|
|
8
|
+
const CODEX_TRANSPORT_RETRY_BASE_DELAY_MS = 250;
|
|
6
9
|
// OpenAI gates new codex models server-side by client_version (each model carries a
|
|
7
10
|
// `minimal_client_version`). Track a recent real Codex CLI release; override via env
|
|
8
11
|
// when OpenAI lifts the gate again before we cut a new release.
|
|
@@ -39,146 +42,198 @@ export function extractChatGptAccountId(accessToken) {
|
|
|
39
42
|
}
|
|
40
43
|
export function createOpenAICodexProvider(options) {
|
|
41
44
|
const sessionId = globalThis.crypto?.randomUUID?.() ?? `bubble_${Date.now()}`;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
let refreshPromise;
|
|
46
|
+
async function resolveRequestAuth(forceRefresh = false) {
|
|
47
|
+
let credentials = await options.auth?.getCredentials();
|
|
48
|
+
if (credentials && options.auth) {
|
|
49
|
+
const expired = options.auth.isExpired
|
|
50
|
+
? options.auth.isExpired(credentials, TOKEN_REFRESH_GRACE_MS)
|
|
51
|
+
: Date.now() >= credentials.expiresAt - TOKEN_REFRESH_GRACE_MS;
|
|
52
|
+
if ((forceRefresh || !credentials.accessToken || expired) && credentials.refreshToken) {
|
|
53
|
+
if (!refreshPromise) {
|
|
54
|
+
refreshPromise = options.auth.refreshCredentials(credentials).finally(() => {
|
|
55
|
+
refreshPromise = undefined;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
credentials = await refreshPromise;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const accessToken = credentials?.accessToken || options.apiKey;
|
|
62
|
+
const accountId = credentials?.accountId || extractChatGptAccountId(accessToken);
|
|
45
63
|
if (!accountId) {
|
|
46
64
|
throw new Error("Failed to extract chatgpt_account_id from ChatGPT OAuth token.");
|
|
47
65
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
66
|
+
return { accessToken, accountId };
|
|
67
|
+
}
|
|
68
|
+
async function* streamChat(messages, chatOptions) {
|
|
69
|
+
const requestConfig = resolveProviderRequestConfig("openai-codex", chatOptions.model, chatOptions.thinkingLevel ?? options.thinkingLevel ?? "off");
|
|
70
|
+
const body = JSON.stringify(buildRequestBody(messages, {
|
|
71
|
+
model: chatOptions.model,
|
|
72
|
+
tools: chatOptions.tools,
|
|
73
|
+
reasoningEffort: requestConfig.reasoningEffort,
|
|
74
|
+
sessionId,
|
|
75
|
+
providerId: options.providerId,
|
|
76
|
+
promptCacheKey: options.promptCacheKey,
|
|
77
|
+
}));
|
|
78
|
+
const sendRequest = async (forceRefresh = false) => {
|
|
79
|
+
const { accessToken, accountId } = await resolveRequestAuth(forceRefresh);
|
|
80
|
+
return fetch(resolveCodexUrl(options.baseURL), buildCodexRequestInit({
|
|
81
|
+
accessToken,
|
|
82
|
+
accountId,
|
|
56
83
|
sessionId,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}))
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
if (type === "response.failed") {
|
|
75
|
-
const message = typeof event.response?.error?.message === "string"
|
|
76
|
-
? event.response.error.message
|
|
77
|
-
: "Codex response failed";
|
|
78
|
-
throw new Error(message);
|
|
79
|
-
}
|
|
80
|
-
if (type === "response.output_item.added") {
|
|
81
|
-
const item = event.item;
|
|
82
|
-
if (item?.type === "function_call" && typeof item.call_id === "string" && typeof item.name === "string") {
|
|
83
|
-
currentToolCall = {
|
|
84
|
-
id: item.call_id,
|
|
85
|
-
name: item.name,
|
|
86
|
-
args: typeof item.arguments === "string" ? item.arguments : "",
|
|
87
|
-
started: true,
|
|
88
|
-
};
|
|
89
|
-
yield {
|
|
90
|
-
type: "tool_call",
|
|
91
|
-
id: currentToolCall.id,
|
|
92
|
-
name: currentToolCall.name,
|
|
93
|
-
arguments: "",
|
|
94
|
-
isStart: true,
|
|
95
|
-
isEnd: false,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
if (type === "response.output_text.delta" || type === "response.refusal.delta") {
|
|
101
|
-
const delta = typeof event.delta === "string" ? event.delta : "";
|
|
102
|
-
if (delta) {
|
|
103
|
-
yield { type: "text", content: delta };
|
|
104
|
-
}
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
if (type === "response.reasoning_summary_text.delta") {
|
|
108
|
-
const delta = typeof event.delta === "string" ? event.delta : "";
|
|
109
|
-
if (delta) {
|
|
110
|
-
yield { type: "reasoning_delta", content: delta };
|
|
111
|
-
}
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
if (type === "response.function_call_arguments.delta" && currentToolCall) {
|
|
115
|
-
const delta = typeof event.delta === "string" ? event.delta : "";
|
|
116
|
-
if (delta) {
|
|
117
|
-
currentToolCall.args += delta;
|
|
118
|
-
yield {
|
|
119
|
-
type: "tool_call",
|
|
120
|
-
id: currentToolCall.id,
|
|
121
|
-
name: currentToolCall.name,
|
|
122
|
-
arguments: delta,
|
|
123
|
-
isStart: false,
|
|
124
|
-
isEnd: false,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
if (type === "response.function_call_arguments.done" && currentToolCall) {
|
|
130
|
-
const finalArgs = typeof event.arguments === "string" ? event.arguments : currentToolCall.args;
|
|
131
|
-
if (finalArgs.startsWith(currentToolCall.args)) {
|
|
132
|
-
const tail = finalArgs.slice(currentToolCall.args.length);
|
|
133
|
-
if (tail) {
|
|
134
|
-
currentToolCall.args = finalArgs;
|
|
135
|
-
yield {
|
|
136
|
-
type: "tool_call",
|
|
137
|
-
id: currentToolCall.id,
|
|
138
|
-
name: currentToolCall.name,
|
|
139
|
-
arguments: tail,
|
|
140
|
-
isStart: false,
|
|
141
|
-
isEnd: false,
|
|
142
|
-
};
|
|
84
|
+
signal: chatOptions.abortSignal,
|
|
85
|
+
body,
|
|
86
|
+
}));
|
|
87
|
+
};
|
|
88
|
+
for (let attempt = 0;; attempt++) {
|
|
89
|
+
let sawParsedSseEvent = false;
|
|
90
|
+
let currentToolCall;
|
|
91
|
+
try {
|
|
92
|
+
let response = await sendRequest();
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const errorText = await response.text().catch(() => "");
|
|
95
|
+
if (response.status === 401 && options.auth && isTokenExpiredError(errorText)) {
|
|
96
|
+
response = await sendRequest(true);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
throw new Error(`${response.status} status code${errorText ? `: ${errorText}` : " (no body)"}`);
|
|
143
100
|
}
|
|
144
101
|
}
|
|
145
|
-
|
|
146
|
-
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
const errorText = await response.text().catch(() => "");
|
|
104
|
+
throw new Error(`${response.status} status code${errorText ? `: ${errorText}` : " (no body)"}`);
|
|
147
105
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
106
|
+
for await (const event of parseSse(response)) {
|
|
107
|
+
sawParsedSseEvent = true;
|
|
108
|
+
const type = typeof event.type === "string" ? event.type : undefined;
|
|
109
|
+
if (!type)
|
|
110
|
+
continue;
|
|
111
|
+
if (type === "error") {
|
|
112
|
+
const message = typeof event.message === "string" ? event.message : JSON.stringify(event);
|
|
113
|
+
throw new Error(message);
|
|
114
|
+
}
|
|
115
|
+
if (type === "response.failed") {
|
|
116
|
+
const message = typeof event.response?.error?.message === "string"
|
|
117
|
+
? event.response.error.message
|
|
118
|
+
: "Codex response failed";
|
|
119
|
+
throw new Error(message);
|
|
120
|
+
}
|
|
121
|
+
if (type === "response.output_item.added") {
|
|
122
|
+
const item = event.item;
|
|
123
|
+
if (item?.type === "function_call" && typeof item.call_id === "string" && typeof item.name === "string") {
|
|
124
|
+
currentToolCall = {
|
|
125
|
+
id: item.call_id,
|
|
126
|
+
name: item.name,
|
|
127
|
+
args: typeof item.arguments === "string" ? item.arguments : "",
|
|
128
|
+
started: true,
|
|
129
|
+
};
|
|
130
|
+
yield {
|
|
131
|
+
type: "tool_call",
|
|
132
|
+
id: currentToolCall.id,
|
|
133
|
+
name: currentToolCall.name,
|
|
134
|
+
arguments: "",
|
|
135
|
+
isStart: true,
|
|
136
|
+
isEnd: false,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (type === "response.output_text.delta" || type === "response.refusal.delta") {
|
|
142
|
+
const delta = typeof event.delta === "string" ? event.delta : "";
|
|
143
|
+
if (delta) {
|
|
144
|
+
yield { type: "text", content: delta };
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (type === "response.reasoning_summary_text.delta") {
|
|
149
|
+
const delta = typeof event.delta === "string" ? event.delta : "";
|
|
150
|
+
if (delta) {
|
|
151
|
+
yield { type: "reasoning_delta", content: delta };
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (type === "response.function_call_arguments.delta" && currentToolCall) {
|
|
156
|
+
const delta = typeof event.delta === "string" ? event.delta : "";
|
|
157
|
+
if (delta) {
|
|
158
|
+
currentToolCall.args += delta;
|
|
159
|
+
yield {
|
|
160
|
+
type: "tool_call",
|
|
161
|
+
id: currentToolCall.id,
|
|
162
|
+
name: currentToolCall.name,
|
|
163
|
+
arguments: delta,
|
|
164
|
+
isStart: false,
|
|
165
|
+
isEnd: false,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (type === "response.function_call_arguments.done" && currentToolCall) {
|
|
171
|
+
const finalArgs = typeof event.arguments === "string" ? event.arguments : currentToolCall.args;
|
|
172
|
+
if (finalArgs.startsWith(currentToolCall.args)) {
|
|
173
|
+
const tail = finalArgs.slice(currentToolCall.args.length);
|
|
174
|
+
if (tail) {
|
|
175
|
+
currentToolCall.args = finalArgs;
|
|
176
|
+
yield {
|
|
177
|
+
type: "tool_call",
|
|
178
|
+
id: currentToolCall.id,
|
|
179
|
+
name: currentToolCall.name,
|
|
180
|
+
arguments: tail,
|
|
181
|
+
isStart: false,
|
|
182
|
+
isEnd: false,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
currentToolCall.args = finalArgs;
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (type === "response.output_item.done" && currentToolCall) {
|
|
192
|
+
const item = event.item;
|
|
193
|
+
if (item?.type === "function_call" && item.call_id === currentToolCall.id) {
|
|
194
|
+
yield {
|
|
195
|
+
type: "tool_call",
|
|
196
|
+
id: currentToolCall.id,
|
|
197
|
+
name: currentToolCall.name,
|
|
198
|
+
arguments: "",
|
|
199
|
+
isStart: false,
|
|
200
|
+
isEnd: true,
|
|
201
|
+
};
|
|
202
|
+
currentToolCall = undefined;
|
|
203
|
+
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (type === "response.completed" || type === "response.done" || type === "response.incomplete") {
|
|
207
|
+
const usage = event.response?.usage;
|
|
208
|
+
if (usage) {
|
|
209
|
+
yield {
|
|
210
|
+
type: "usage",
|
|
211
|
+
usage: normalizeOpenAICodexUsage(usage),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
162
216
|
}
|
|
163
|
-
|
|
217
|
+
yield { type: "done" };
|
|
218
|
+
return;
|
|
164
219
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
220
|
+
catch (error) {
|
|
221
|
+
if (!shouldRetryCodexTransportError({
|
|
222
|
+
error,
|
|
223
|
+
attempt,
|
|
224
|
+
sawParsedSseEvent,
|
|
225
|
+
signal: chatOptions.abortSignal,
|
|
226
|
+
})) {
|
|
227
|
+
throw error;
|
|
172
228
|
}
|
|
173
|
-
|
|
229
|
+
await sleepBeforeCodexRetry(codexRetryDelayMs(attempt), chatOptions.abortSignal);
|
|
174
230
|
}
|
|
175
231
|
}
|
|
176
|
-
yield { type: "done" };
|
|
177
232
|
}
|
|
178
233
|
async function complete(messages, chatOptions) {
|
|
179
234
|
let content = "";
|
|
180
235
|
for await (const chunk of streamChat(messages, {
|
|
181
|
-
model: chatOptions?.model ?? "gpt-5.
|
|
236
|
+
model: chatOptions?.model ?? "gpt-5.5",
|
|
182
237
|
temperature: chatOptions?.temperature,
|
|
183
238
|
thinkingLevel: chatOptions?.thinkingLevel,
|
|
184
239
|
abortSignal: chatOptions?.abortSignal,
|
|
@@ -191,6 +246,9 @@ export function createOpenAICodexProvider(options) {
|
|
|
191
246
|
}
|
|
192
247
|
return { streamChat, complete };
|
|
193
248
|
}
|
|
249
|
+
function isTokenExpiredError(errorText) {
|
|
250
|
+
return /token_expired|session expired/i.test(errorText);
|
|
251
|
+
}
|
|
194
252
|
export function normalizeOpenAICodexUsage(usage) {
|
|
195
253
|
const promptTokens = typeof usage?.input_tokens === "number" ? usage.input_tokens : 0;
|
|
196
254
|
const cachedTokens = typeof usage?.input_tokens_details?.cached_tokens === "number"
|
|
@@ -359,6 +417,89 @@ async function* parseSse(response) {
|
|
|
359
417
|
}
|
|
360
418
|
}
|
|
361
419
|
}
|
|
420
|
+
function buildCodexRequestInit(options) {
|
|
421
|
+
const init = {
|
|
422
|
+
method: "POST",
|
|
423
|
+
headers: buildSseHeaders(options.accessToken, options.accountId, options.sessionId),
|
|
424
|
+
signal: options.signal,
|
|
425
|
+
body: options.body,
|
|
426
|
+
keepalive: false,
|
|
427
|
+
};
|
|
428
|
+
if (/^(1|true|yes)$/i.test(process.env.BUBBLE_CODEX_FETCH_VERBOSE ?? "")) {
|
|
429
|
+
init.verbose = true;
|
|
430
|
+
}
|
|
431
|
+
return init;
|
|
432
|
+
}
|
|
433
|
+
function shouldRetryCodexTransportError(input) {
|
|
434
|
+
if (input.signal?.aborted)
|
|
435
|
+
return false;
|
|
436
|
+
if (input.sawParsedSseEvent)
|
|
437
|
+
return false;
|
|
438
|
+
if (input.attempt >= CODEX_TRANSPORT_MAX_RETRIES)
|
|
439
|
+
return false;
|
|
440
|
+
return isTransientCodexTransportError(input.error);
|
|
441
|
+
}
|
|
442
|
+
function isTransientCodexTransportError(error) {
|
|
443
|
+
const text = errorMessageChain(error).join("\n");
|
|
444
|
+
if (/\bAbortError\b/i.test(text))
|
|
445
|
+
return false;
|
|
446
|
+
return [
|
|
447
|
+
/The socket connection was closed unexpectedly/i,
|
|
448
|
+
/\bConnectionClosed\b/i,
|
|
449
|
+
/\bECONNRESET\b/i,
|
|
450
|
+
/\bUND_ERR_SOCKET\b/i,
|
|
451
|
+
/\bEPIPE\b/i,
|
|
452
|
+
/socket hang up/i,
|
|
453
|
+
/fetch failed/i,
|
|
454
|
+
].some((pattern) => pattern.test(text));
|
|
455
|
+
}
|
|
456
|
+
function errorMessageChain(error) {
|
|
457
|
+
const messages = [];
|
|
458
|
+
let current = error;
|
|
459
|
+
for (let depth = 0; current && depth < 6; depth++) {
|
|
460
|
+
if (current instanceof Error) {
|
|
461
|
+
messages.push(current.name, current.message);
|
|
462
|
+
current = current.cause;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (typeof current === "object") {
|
|
466
|
+
const record = current;
|
|
467
|
+
for (const key of ["name", "code", "message"]) {
|
|
468
|
+
if (typeof record[key] === "string")
|
|
469
|
+
messages.push(record[key]);
|
|
470
|
+
}
|
|
471
|
+
current = record.cause;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
messages.push(String(current));
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
return messages;
|
|
478
|
+
}
|
|
479
|
+
function codexRetryDelayMs(attempt) {
|
|
480
|
+
return CODEX_TRANSPORT_RETRY_BASE_DELAY_MS * Math.pow(3, attempt);
|
|
481
|
+
}
|
|
482
|
+
function sleepBeforeCodexRetry(ms, signal) {
|
|
483
|
+
if (signal?.aborted)
|
|
484
|
+
return Promise.reject(toAbortError(signal));
|
|
485
|
+
return new Promise((resolve, reject) => {
|
|
486
|
+
const onAbort = () => {
|
|
487
|
+
clearTimeout(timeout);
|
|
488
|
+
signal?.removeEventListener("abort", onAbort);
|
|
489
|
+
reject(toAbortError(signal));
|
|
490
|
+
};
|
|
491
|
+
const timeout = setTimeout(() => {
|
|
492
|
+
signal?.removeEventListener("abort", onAbort);
|
|
493
|
+
resolve();
|
|
494
|
+
}, ms);
|
|
495
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
function toAbortError(signal) {
|
|
499
|
+
if (signal?.reason instanceof Error)
|
|
500
|
+
return signal.reason;
|
|
501
|
+
return new DOMException(typeof signal?.reason === "string" ? signal.reason : "Aborted", "AbortError");
|
|
502
|
+
}
|
|
362
503
|
function buildBaseHeaders(accessToken, accountId, sessionId, extraHeaders) {
|
|
363
504
|
const headers = new Headers(extraHeaders);
|
|
364
505
|
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { UserConfig } from "./config.js";
|
|
8
8
|
import { ModelConfig } from "./model-config.js";
|
|
9
9
|
import { AuthStorage } from "./oauth/index.js";
|
|
10
|
+
import { type OpenAICodexAuthAdapter } from "./provider-openai-codex.js";
|
|
10
11
|
export interface ProviderProfile {
|
|
11
12
|
id: string;
|
|
12
13
|
name: string;
|
|
@@ -32,6 +33,7 @@ export declare class ProviderRegistry {
|
|
|
32
33
|
getAuthStorage(): AuthStorage;
|
|
33
34
|
supportsOAuth(providerId: string): boolean;
|
|
34
35
|
private resolveOAuthAuthKey;
|
|
36
|
+
createOpenAICodexAuthAdapter(providerId: string): OpenAICodexAuthAdapter | undefined;
|
|
35
37
|
getDefaultModel(providerId: string, authType?: ProviderProfile["authType"]): string | undefined;
|
|
36
38
|
prepareProvider(providerId: string): Promise<void>;
|
|
37
39
|
getConfigured(): ProviderProfile[];
|
|
@@ -35,11 +35,54 @@ export class ProviderRegistry {
|
|
|
35
35
|
return !!getBuiltinProvider(providerId)?.supportsOAuth;
|
|
36
36
|
}
|
|
37
37
|
resolveOAuthAuthKey(providerId) {
|
|
38
|
-
if (providerId === "openai"
|
|
39
|
-
|
|
38
|
+
if (providerId === "openai" || providerId === "openai-codex") {
|
|
39
|
+
if (this.authStorage.has("openai"))
|
|
40
|
+
return "openai";
|
|
41
|
+
if (this.authStorage.has("openai-codex"))
|
|
42
|
+
return "openai-codex";
|
|
40
43
|
}
|
|
41
44
|
return providerId;
|
|
42
45
|
}
|
|
46
|
+
createOpenAICodexAuthAdapter(providerId) {
|
|
47
|
+
if (providerId !== "openai" && providerId !== "openai-codex")
|
|
48
|
+
return undefined;
|
|
49
|
+
if (!this.authStorage.has(this.resolveOAuthAuthKey(providerId)))
|
|
50
|
+
return undefined;
|
|
51
|
+
const readCredentials = () => this.authStorage.get(this.resolveOAuthAuthKey(providerId));
|
|
52
|
+
let refreshPromise;
|
|
53
|
+
return {
|
|
54
|
+
getCredentials: readCredentials,
|
|
55
|
+
isExpired: (_credentials, graceMs) => this.authStorage.isExpired(this.resolveOAuthAuthKey(providerId), graceMs),
|
|
56
|
+
refreshCredentials: async () => {
|
|
57
|
+
if (!refreshPromise) {
|
|
58
|
+
refreshPromise = (async () => {
|
|
59
|
+
const authKey = this.resolveOAuthAuthKey(providerId);
|
|
60
|
+
const current = this.authStorage.get(authKey);
|
|
61
|
+
if (!current?.refreshToken) {
|
|
62
|
+
throw new Error(`OpenAI OAuth credentials for ${providerId} are missing a refresh token.`);
|
|
63
|
+
}
|
|
64
|
+
const refreshed = await refreshOpenAICodex(current.refreshToken);
|
|
65
|
+
const next = {
|
|
66
|
+
type: "oauth",
|
|
67
|
+
accessToken: refreshed.accessToken,
|
|
68
|
+
refreshToken: refreshed.refreshToken,
|
|
69
|
+
expiresAt: refreshed.expiresAt,
|
|
70
|
+
idToken: refreshed.idToken || current.idToken,
|
|
71
|
+
accountId: refreshed.accountId || current.accountId,
|
|
72
|
+
};
|
|
73
|
+
this.authStorage.set("openai", next);
|
|
74
|
+
if (authKey !== "openai") {
|
|
75
|
+
this.authStorage.set(authKey, next);
|
|
76
|
+
}
|
|
77
|
+
return next;
|
|
78
|
+
})().finally(() => {
|
|
79
|
+
refreshPromise = undefined;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return refreshPromise;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
43
86
|
getDefaultModel(providerId, authType = "api") {
|
|
44
87
|
const customModels = this.modelConfig.getCustomModels(providerId);
|
|
45
88
|
if (customModels.length > 0) {
|
|
@@ -52,18 +95,22 @@ export class ProviderRegistry {
|
|
|
52
95
|
}
|
|
53
96
|
async prepareProvider(providerId) {
|
|
54
97
|
const authKey = this.resolveOAuthAuthKey(providerId);
|
|
55
|
-
if (providerId === "openai" && this.authStorage.isExpired(authKey)) {
|
|
98
|
+
if ((providerId === "openai" || providerId === "openai-codex") && this.authStorage.isExpired(authKey)) {
|
|
56
99
|
const creds = this.authStorage.get(authKey);
|
|
57
100
|
if (creds?.refreshToken) {
|
|
58
101
|
const refreshed = await refreshOpenAICodex(creds.refreshToken);
|
|
59
|
-
|
|
102
|
+
const next = {
|
|
60
103
|
type: "oauth",
|
|
61
104
|
accessToken: refreshed.accessToken,
|
|
62
105
|
refreshToken: refreshed.refreshToken,
|
|
63
106
|
expiresAt: refreshed.expiresAt,
|
|
64
|
-
idToken: refreshed.idToken,
|
|
65
|
-
accountId: refreshed.accountId,
|
|
66
|
-
}
|
|
107
|
+
idToken: refreshed.idToken || creds.idToken,
|
|
108
|
+
accountId: refreshed.accountId || creds.accountId,
|
|
109
|
+
};
|
|
110
|
+
this.authStorage.set("openai", next);
|
|
111
|
+
if (authKey !== "openai") {
|
|
112
|
+
this.authStorage.set(authKey, next);
|
|
113
|
+
}
|
|
67
114
|
}
|
|
68
115
|
}
|
|
69
116
|
}
|
|
@@ -194,9 +241,11 @@ export class ProviderRegistry {
|
|
|
194
241
|
}
|
|
195
242
|
if (provider.id === "openai" && provider.authType === "oauth" && provider.apiKey) {
|
|
196
243
|
try {
|
|
244
|
+
await this.prepareProvider(provider.id);
|
|
245
|
+
const currentProvider = this.getConfigured().find((p) => p.id === provider.id) || provider;
|
|
197
246
|
const descriptors = await fetchOpenAICodexModels({
|
|
198
|
-
baseURL:
|
|
199
|
-
accessToken:
|
|
247
|
+
baseURL: currentProvider.baseURL,
|
|
248
|
+
accessToken: currentProvider.apiKey,
|
|
200
249
|
});
|
|
201
250
|
const visible = descriptors.filter((d) => d.visibility !== "hide");
|
|
202
251
|
if (visible.length > 0) {
|
package/dist/provider.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Works with OpenRouter, OpenAI, DeepSeek, Google, Groq, Together, and local OpenAI-compatible endpoints.
|
|
5
5
|
*/
|
|
6
|
+
import { type OpenAICodexAuthAdapter } from "./provider-openai-codex.js";
|
|
6
7
|
import type { Provider, ProviderMessage, StreamChunk, ThinkingLevel } from "./types.js";
|
|
7
8
|
type ReasoningContentEcho = "tool_calls" | "all" | "none";
|
|
8
9
|
export type ToolArgsMergeMode = "delta" | "snapshot";
|
|
@@ -23,6 +24,8 @@ export interface ProviderInstanceOptions {
|
|
|
23
24
|
thinkingLevel?: ThinkingLevel;
|
|
24
25
|
/** Stable per-session seed for provider prompt caches. */
|
|
25
26
|
promptCacheKey?: string;
|
|
27
|
+
/** Dynamic OAuth access-token loader/refresh hook for ChatGPT Codex requests. */
|
|
28
|
+
openAICodexAuth?: OpenAICodexAuthAdapter;
|
|
26
29
|
}
|
|
27
30
|
export declare function createUnavailableProvider(message: string): Provider;
|
|
28
31
|
export declare function createProviderInstance(options: ProviderInstanceOptions): Provider;
|
package/dist/provider.js
CHANGED
|
@@ -66,7 +66,11 @@ export function createUnavailableProvider(message) {
|
|
|
66
66
|
}
|
|
67
67
|
export function createProviderInstance(options) {
|
|
68
68
|
if (isOpenAICodexBaseUrl(options.baseURL)) {
|
|
69
|
-
return createOpenAICodexProvider({
|
|
69
|
+
return createOpenAICodexProvider({
|
|
70
|
+
...options,
|
|
71
|
+
providerId: options.providerId || "openai-codex",
|
|
72
|
+
auth: options.openAICodexAuth,
|
|
73
|
+
});
|
|
70
74
|
}
|
|
71
75
|
const client = new OpenAI({
|
|
72
76
|
apiKey: options.apiKey,
|
package/dist/session-log.js
CHANGED
|
@@ -191,6 +191,11 @@ function normalizeMessageToEntries(message, id, timestamp) {
|
|
|
191
191
|
role: "assistant",
|
|
192
192
|
content: message.content,
|
|
193
193
|
reasoning: message.reasoning,
|
|
194
|
+
model: message.model,
|
|
195
|
+
providerId: message.providerId,
|
|
196
|
+
modelId: message.modelId,
|
|
197
|
+
usage: message.usage,
|
|
198
|
+
error: message.error,
|
|
194
199
|
},
|
|
195
200
|
timestamp,
|
|
196
201
|
};
|
|
@@ -228,7 +233,14 @@ function isSessionLogEntry(entry) {
|
|
|
228
233
|
].includes(entry.type);
|
|
229
234
|
}
|
|
230
235
|
function nextEntryId(entries) {
|
|
231
|
-
|
|
236
|
+
let max = 0;
|
|
237
|
+
for (const entry of entries) {
|
|
238
|
+
const match = /^(\d+)/.exec(entry.id);
|
|
239
|
+
if (!match)
|
|
240
|
+
continue;
|
|
241
|
+
max = Math.max(max, Number(match[1]));
|
|
242
|
+
}
|
|
243
|
+
return `${max + 1}`;
|
|
232
244
|
}
|
|
233
245
|
function cloneMessage(message) {
|
|
234
246
|
if (message.role === "assistant") {
|
|
@@ -7,6 +7,7 @@ import { encodeModel, decodeModel, displayModel, BUILTIN_PROVIDERS, isUserVisibl
|
|
|
7
7
|
import { getAvailableThinkingLevels, normalizeThinkingLevel } from "../provider-transform.js";
|
|
8
8
|
import { buildSystemPrompt } from "../system-prompt.js";
|
|
9
9
|
import { isThinkingLevel } from "../variant/thinking-level.js";
|
|
10
|
+
import { collectUsageStatsBundle, formatStatsText } from "../stats/usage.js";
|
|
10
11
|
import { buildMemoryPrompt, getMemoryStatus, isMemoryDisabled, resetMemory, searchMemory, } from "../memory/index.js";
|
|
11
12
|
import { feishuCommand } from "./feishu.js";
|
|
12
13
|
const VALID_SCOPES = ["user", "project", "local"];
|
|
@@ -795,6 +796,17 @@ const builtinSlashCommandEntries = [
|
|
|
795
796
|
return `✓ Compaction complete · ${dropped} log entr${dropped === 1 ? "y" : "ies"} summarized`;
|
|
796
797
|
},
|
|
797
798
|
},
|
|
799
|
+
{
|
|
800
|
+
name: "stats",
|
|
801
|
+
description: "Show recent model usage statistics",
|
|
802
|
+
async handler(_args, ctx) {
|
|
803
|
+
if (ctx.openStats) {
|
|
804
|
+
ctx.openStats();
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
return formatStatsText(collectUsageStatsBundle());
|
|
808
|
+
},
|
|
809
|
+
},
|
|
798
810
|
{
|
|
799
811
|
name: "feedback",
|
|
800
812
|
description: "Send feedback or report a bug to Bubble developers",
|
|
@@ -46,6 +46,8 @@ export interface SlashCommandContext {
|
|
|
46
46
|
setSidebarMode?: (mode: SidebarMode) => SidebarCommandState;
|
|
47
47
|
/** Open the feedback dialog. `initialDescription` prefills the description field. */
|
|
48
48
|
openFeedback?: (initialDescription: string) => void;
|
|
49
|
+
/** Open the interactive usage stats panel. */
|
|
50
|
+
openStats?: () => void;
|
|
49
51
|
}
|
|
50
52
|
/**
|
|
51
53
|
* Return types for a slash command handler:
|