codex-grok-bridge 1.7.0 → 1.7.2
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/CHANGELOG.md +13 -1
- package/README.md +129 -34
- package/package.json +1 -1
- package/src/bridge.mjs +135 -22
- package/src/errors.mjs +8 -1
- package/src/imagine.mjs +150 -0
- package/src/proxy.mjs +235 -36
- package/src/runtime.mjs +5 -8
- package/src/tools.mjs +256 -27
- package/src/videogen.mjs +196 -0
package/src/imagine.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Codex posts OpenAI Images requests on the provider base
|
|
2
|
+
// (`POST /v1/images/generations`, `POST /v1/images/edits`). grok-build sends
|
|
3
|
+
// the same two paths to the Imagine API with the login bearer, not an
|
|
4
|
+
// XAI_API_KEY. The bodies are not the same: Codex hardcodes `gpt-image-2`
|
|
5
|
+
// plus `size` / `quality` / `background`, and edits use `{image_url}` or
|
|
6
|
+
// `{file_id}`. Imagine wants `grok-imagine-image-quality`, `response_format:
|
|
7
|
+
// b64_json`, and `{url}` references.
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_IMAGINE_API_BASE = "https://api.x.ai/v1";
|
|
10
|
+
export const IMAGINE_MODEL = "grok-imagine-image-quality";
|
|
11
|
+
|
|
12
|
+
const IMAGINE_TIMEOUT_MS = 300_000;
|
|
13
|
+
|
|
14
|
+
function promptOf(input) {
|
|
15
|
+
const prompt = typeof input?.prompt === "string" ? input.prompt.trim() : "";
|
|
16
|
+
if (!prompt) {
|
|
17
|
+
const error = new Error("Image generation requires a prompt.");
|
|
18
|
+
error.status = 400;
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
return prompt;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function basePayload(prompt) {
|
|
25
|
+
return {
|
|
26
|
+
model: IMAGINE_MODEL,
|
|
27
|
+
prompt,
|
|
28
|
+
n: 1,
|
|
29
|
+
resolution: "1k",
|
|
30
|
+
response_format: "b64_json",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function imagineGenerationBody(input) {
|
|
35
|
+
return basePayload(promptOf(input));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function imagineEditBody(input) {
|
|
39
|
+
const prompt = promptOf(input);
|
|
40
|
+
const images = Array.isArray(input?.images) ? input.images : [];
|
|
41
|
+
const urls = [];
|
|
42
|
+
for (const image of images) {
|
|
43
|
+
if (!image || typeof image !== "object") continue;
|
|
44
|
+
if (typeof image.image_url === "string" && image.image_url.trim()) {
|
|
45
|
+
urls.push(image.image_url.trim());
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (typeof image.file_id === "string" && image.file_id.trim()) {
|
|
49
|
+
const error = new Error("OpenAI file ids cannot be edited with Grok.");
|
|
50
|
+
error.status = 400;
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!urls.length) {
|
|
55
|
+
const error = new Error("Image edit requires a reference image.");
|
|
56
|
+
error.status = 400;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
const payload = basePayload(prompt);
|
|
60
|
+
if (urls.length === 1) payload.image = { url: urls[0] };
|
|
61
|
+
else {
|
|
62
|
+
payload.images = urls.map((url) => ({ url }));
|
|
63
|
+
payload.aspect_ratio = "auto";
|
|
64
|
+
}
|
|
65
|
+
return payload;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function codexImageResponse(payload) {
|
|
69
|
+
const data = [];
|
|
70
|
+
if (Array.isArray(payload?.data)) {
|
|
71
|
+
for (const item of payload.data) {
|
|
72
|
+
if (item && typeof item.b64_json === "string" && item.b64_json.length)
|
|
73
|
+
data.push({ b64_json: item.b64_json });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!data.length) {
|
|
77
|
+
const error = new Error("Image generation returned no image.");
|
|
78
|
+
error.status = 502;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
const created = Number.isInteger(payload?.created)
|
|
82
|
+
? payload.created
|
|
83
|
+
: Math.floor(Date.now() / 1000);
|
|
84
|
+
return { created, data };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function scrub(text, token) {
|
|
88
|
+
let out = String(text ?? "");
|
|
89
|
+
out = out.replace(/Bearer\s+\S+/gi, "Bearer [redacted]");
|
|
90
|
+
if (token) out = out.split(token).join("[redacted]");
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function imagineHttp(fetchImpl, url, init) {
|
|
95
|
+
const response = await fetchImpl(url, { ...init, redirect: "manual" });
|
|
96
|
+
const text = await response.text().catch(() => "");
|
|
97
|
+
if (response.status >= 300 && response.status < 400) {
|
|
98
|
+
const error = new Error("Image generation failed (redirect).");
|
|
99
|
+
error.status = 502;
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
let body = null;
|
|
103
|
+
if (text) {
|
|
104
|
+
try {
|
|
105
|
+
body = JSON.parse(text);
|
|
106
|
+
} catch {
|
|
107
|
+
body = null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { status: response.status, body };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function forwardImagine(options) {
|
|
114
|
+
const token = options.token;
|
|
115
|
+
const kind = options.kind === "edits" ? "edits" : "generations";
|
|
116
|
+
const payload =
|
|
117
|
+
kind === "edits" ? imagineEditBody(options.body) : imagineGenerationBody(options.body);
|
|
118
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
119
|
+
const base = (options.baseUrl ?? DEFAULT_IMAGINE_API_BASE).replace(/\/$/, "");
|
|
120
|
+
const timeout = AbortSignal.timeout(IMAGINE_TIMEOUT_MS);
|
|
121
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
122
|
+
let result;
|
|
123
|
+
try {
|
|
124
|
+
result = await imagineHttp(fetchImpl, `${base}/images/${kind}`, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: {
|
|
127
|
+
authorization: `Bearer ${token}`,
|
|
128
|
+
"content-type": "application/json",
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify(payload),
|
|
131
|
+
signal,
|
|
132
|
+
});
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error?.status) throw error;
|
|
135
|
+
const wrapped = new Error(scrub(error?.message || "Image generation failed.", token));
|
|
136
|
+
wrapped.status = 502;
|
|
137
|
+
throw wrapped;
|
|
138
|
+
}
|
|
139
|
+
if (result.status === 401 || result.status === 403) {
|
|
140
|
+
const error = new Error("Grok login expired. Run grok login.");
|
|
141
|
+
error.status = 401;
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
if (result.status < 200 || result.status >= 300) {
|
|
145
|
+
const error = new Error(`Image generation failed (${result.status}).`);
|
|
146
|
+
error.status = result.status >= 400 && result.status < 500 ? result.status : 502;
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
return codexImageResponse(result.body);
|
|
150
|
+
}
|
package/src/proxy.mjs
CHANGED
|
@@ -8,8 +8,13 @@ import { BRIDGE_ERROR, classifyBridgeError } from "./errors.mjs";
|
|
|
8
8
|
export const DEFAULT_PROXY_BASE = "https://cli-chat-proxy.grok.com/v1";
|
|
9
9
|
export const DEFAULT_CLIENT_IDENTIFIER = "grok-shell";
|
|
10
10
|
|
|
11
|
+
const UNKNOWN_CLIENT_VERSION = "unknown";
|
|
11
12
|
let cachedClientVersion;
|
|
12
13
|
|
|
14
|
+
export function clearClientVersionCache() {
|
|
15
|
+
cachedClientVersion = undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
export function detectGrokClientVersion(home = homedir()) {
|
|
14
19
|
if (cachedClientVersion) return cachedClientVersion;
|
|
15
20
|
try {
|
|
@@ -20,9 +25,9 @@ export function detectGrokClientVersion(home = homedir()) {
|
|
|
20
25
|
timeout: 5000,
|
|
21
26
|
});
|
|
22
27
|
const match = output.match(/grok\s+(\S+(?:\s+\([^)]+\))?)/i);
|
|
23
|
-
cachedClientVersion = match ? match[1].trim() :
|
|
28
|
+
cachedClientVersion = match ? match[1].trim() : UNKNOWN_CLIENT_VERSION;
|
|
24
29
|
} catch {
|
|
25
|
-
cachedClientVersion =
|
|
30
|
+
cachedClientVersion = UNKNOWN_CLIENT_VERSION;
|
|
26
31
|
}
|
|
27
32
|
return cachedClientVersion;
|
|
28
33
|
}
|
|
@@ -74,9 +79,9 @@ export async function openProxyStream(options) {
|
|
|
74
79
|
return response;
|
|
75
80
|
}
|
|
76
81
|
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
82
|
+
// The upstream reply is already buffered until it finishes. This wait applies
|
|
83
|
+
// only while that buffer is flushed: a slow Codex exerts backpressure, and a
|
|
84
|
+
// client that vanishes is noticed instead of being written forever.
|
|
80
85
|
function drained(output) {
|
|
81
86
|
return new Promise((resolve, reject) => {
|
|
82
87
|
const settle = (fn, value) => {
|
|
@@ -100,24 +105,68 @@ function drained(output) {
|
|
|
100
105
|
});
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
});
|
|
108
|
-
if (!output.write(block)) await drained(output);
|
|
109
|
-
}
|
|
108
|
+
// First try, then two more. That is the budget Codex used to spend on
|
|
109
|
+
// request_max_retries / stream_max_retries. A retry sends the prompt again, so
|
|
110
|
+
// the input tokens for that attempt can be billed again.
|
|
111
|
+
export const PROXY_ATTEMPTS = 3;
|
|
110
112
|
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
113
|
+
// DNS and connect failures are included. A socket reset (ECONNRESET, EPIPE,
|
|
114
|
+
// UND_ERR_SOCKET) or a body that ends before response.completed is included
|
|
115
|
+
// only while Codex has not been sent a byte. User aborts and deterministic
|
|
116
|
+
// 422s are not in this set.
|
|
114
117
|
const RETRYABLE = new Set([
|
|
115
118
|
BRIDGE_ERROR.DNS,
|
|
116
119
|
BRIDGE_ERROR.CONNECT,
|
|
117
120
|
BRIDGE_ERROR.UPSTREAM_CLOSED,
|
|
118
121
|
]);
|
|
119
122
|
|
|
120
|
-
|
|
123
|
+
// response.incomplete is the other usage event this code already treats as the
|
|
124
|
+
// end of a response. response.failed here is a finished upstream event, not a
|
|
125
|
+
// dropped socket, so it is forwarded instead of retried.
|
|
126
|
+
const TERMINAL_EVENTS = new Set([
|
|
127
|
+
"response.completed",
|
|
128
|
+
"response.incomplete",
|
|
129
|
+
"response.failed",
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
export function isTerminalSseBlock(part) {
|
|
133
|
+
for (const line of part.split("\n")) {
|
|
134
|
+
if (line.startsWith("event:") && TERMINAL_EVENTS.has(line.slice(6).trim()))
|
|
135
|
+
return true;
|
|
136
|
+
if (!line.startsWith("data:")) continue;
|
|
137
|
+
const payload = line.slice(5).trim();
|
|
138
|
+
if (!payload || payload === "[DONE]") continue;
|
|
139
|
+
try {
|
|
140
|
+
const value = JSON.parse(payload);
|
|
141
|
+
if (TERMINAL_EVENTS.has(value?.type)) return true;
|
|
142
|
+
} catch {
|
|
143
|
+
// A non-JSON data line cannot be the terminal event.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function prematureUpstreamClose() {
|
|
150
|
+
return Object.assign(new Error("Grok closed the connection before finishing"), {
|
|
151
|
+
code: "ERR_UPSTREAM_PREMATURE_CLOSE",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function clientClosed() {
|
|
156
|
+
return Object.assign(new Error("The client closed the stream"), {
|
|
157
|
+
code: "ERR_STREAM_PREMATURE_CLOSE",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Returns the retry kind, or null when this failure must surface as-is.
|
|
162
|
+
function retryKind(error, options, attempt, rounds, clientBytes) {
|
|
163
|
+
if (clientBytes > 0 || options.signal?.aborted || attempt === rounds)
|
|
164
|
+
return null;
|
|
165
|
+
const kind = classifyBridgeError(error);
|
|
166
|
+
return RETRYABLE.has(kind) ? kind : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function openProxyStreamWithRetry(options, attempts = PROXY_ATTEMPTS) {
|
|
121
170
|
const rounds = Math.max(1, attempts);
|
|
122
171
|
let lastError;
|
|
123
172
|
for (let attempt = 1; attempt <= rounds; attempt += 1) {
|
|
@@ -125,46 +174,196 @@ export async function openProxyStreamWithRetry(options, attempts = 2) {
|
|
|
125
174
|
return await openProxyStream(options);
|
|
126
175
|
} catch (error) {
|
|
127
176
|
lastError = error;
|
|
128
|
-
const kind =
|
|
129
|
-
if (
|
|
130
|
-
!RETRYABLE.has(kind) ||
|
|
131
|
-
options.signal?.aborted ||
|
|
132
|
-
attempt === rounds
|
|
133
|
-
)
|
|
134
|
-
break;
|
|
177
|
+
const kind = retryKind(error, options, attempt, rounds, 0);
|
|
178
|
+
if (!kind) break;
|
|
135
179
|
options.onRetry?.({ attempt, kind });
|
|
136
180
|
}
|
|
137
181
|
}
|
|
138
182
|
throw lastError;
|
|
139
183
|
}
|
|
140
184
|
|
|
141
|
-
|
|
185
|
+
function completeBlocks(buffer) {
|
|
186
|
+
const parts = buffer.split("\n\n");
|
|
187
|
+
const rest = parts.pop() ?? "";
|
|
188
|
+
return { parts: parts.filter((part) => part.trim()), rest };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Read the upstream body to memory. Write to Codex only after a terminal
|
|
192
|
+
// event, so a reset before that has not shown Codex a partial reply.
|
|
193
|
+
export async function pipeProxySse(stream, output, map, usageBox, onClientByte) {
|
|
142
194
|
const reader = stream.getReader();
|
|
143
195
|
const decoder = new TextDecoder();
|
|
144
196
|
let buffer = "";
|
|
145
197
|
const rewrite = createSseRewriter(map);
|
|
198
|
+
const pending = [];
|
|
199
|
+
let clientBytes = 0;
|
|
200
|
+
let finished = false;
|
|
201
|
+
let failed = false;
|
|
202
|
+
|
|
203
|
+
const flush = async (parts) => {
|
|
204
|
+
for (const part of parts) {
|
|
205
|
+
const rewritten = rewrite(part);
|
|
206
|
+
if (rewritten === null) continue;
|
|
207
|
+
const block = `${rewritten}\n\n`;
|
|
208
|
+
if (output.destroyed || output.writableEnded) throw clientClosed();
|
|
209
|
+
// Count the byte as soon as the client stream accepts it. A later drain
|
|
210
|
+
// error must not look like "Codex saw nothing" and resubmit the prompt.
|
|
211
|
+
const accepted = output.write(block);
|
|
212
|
+
clientBytes += Buffer.byteLength(block);
|
|
213
|
+
onClientByte?.();
|
|
214
|
+
if (!accepted) await drained(output);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const accept = async (parts) => {
|
|
219
|
+
if (!parts.length) return;
|
|
220
|
+
if (finished) {
|
|
221
|
+
await flush(parts);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const terminalAt = parts.findIndex((part) => isTerminalSseBlock(part));
|
|
225
|
+
if (terminalAt === -1) {
|
|
226
|
+
pending.push(...parts);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
pending.push(...parts.slice(0, terminalAt + 1));
|
|
230
|
+
await flush(pending);
|
|
231
|
+
pending.length = 0;
|
|
232
|
+
finished = true;
|
|
233
|
+
await flush(parts.slice(terminalAt + 1));
|
|
234
|
+
};
|
|
235
|
+
|
|
146
236
|
try {
|
|
147
237
|
while (true) {
|
|
238
|
+
if (output.destroyed || output.writableEnded) throw clientClosed();
|
|
148
239
|
const { done, value } = await reader.read();
|
|
149
240
|
if (done) break;
|
|
150
241
|
buffer += decoder.decode(value, { stream: true });
|
|
151
|
-
const
|
|
152
|
-
buffer =
|
|
153
|
-
|
|
154
|
-
if (!part.trim()) continue;
|
|
155
|
-
const rewritten = rewrite(part);
|
|
156
|
-
if (rewritten !== null) await writeBlock(output, rewritten + "\n\n");
|
|
157
|
-
}
|
|
242
|
+
const split = completeBlocks(buffer);
|
|
243
|
+
buffer = split.rest;
|
|
244
|
+
await accept(split.parts);
|
|
158
245
|
}
|
|
159
246
|
buffer += decoder.decode();
|
|
160
|
-
if (buffer.trim())
|
|
161
|
-
|
|
162
|
-
if (rewritten !== null) await writeBlock(output, rewritten + "\n\n");
|
|
163
|
-
}
|
|
247
|
+
if (buffer.trim()) await accept([buffer]);
|
|
248
|
+
if (!finished) throw prematureUpstreamClose();
|
|
164
249
|
} catch (error) {
|
|
165
|
-
|
|
250
|
+
failed = true;
|
|
251
|
+
// Stop pulling from Grok the moment this attempt is over; leaving the body
|
|
166
252
|
// unread holds the upstream socket open for the rest of the response.
|
|
167
253
|
await reader.cancel(error).catch(() => {});
|
|
254
|
+
if (error && typeof error === "object") error.clientBytes = clientBytes;
|
|
255
|
+
throw error;
|
|
256
|
+
} finally {
|
|
257
|
+
if (!failed) reader.releaseLock();
|
|
258
|
+
if (usageBox) usageBox.cacheUsage = rewrite.cacheUsage ?? null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Open and read one attempt. If the socket dies before Codex has a byte, send
|
|
263
|
+
// the same request again. Once a byte has been written, never resubmit.
|
|
264
|
+
async function relayAttempts(options, consume, attempts = PROXY_ATTEMPTS) {
|
|
265
|
+
const rounds = Math.max(1, attempts);
|
|
266
|
+
let lastError;
|
|
267
|
+
for (let attempt = 1; attempt <= rounds; attempt += 1) {
|
|
268
|
+
try {
|
|
269
|
+
const response = await openProxyStream(options);
|
|
270
|
+
await consume(response);
|
|
271
|
+
return response;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
lastError = error;
|
|
274
|
+
const kind = retryKind(
|
|
275
|
+
error,
|
|
276
|
+
options,
|
|
277
|
+
attempt,
|
|
278
|
+
rounds,
|
|
279
|
+
error?.clientBytes ?? 0,
|
|
280
|
+
);
|
|
281
|
+
if (!kind) break;
|
|
282
|
+
options.onRetry?.({ attempt, kind });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
throw lastError;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function relayProxySse(options, output, map, usageBox, attempts = PROXY_ATTEMPTS) {
|
|
289
|
+
return relayAttempts(
|
|
290
|
+
options,
|
|
291
|
+
(response) =>
|
|
292
|
+
pipeProxySse(response.body, output, map, usageBox, options.onClientByte),
|
|
293
|
+
attempts,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Read until response.completed (or the other terminal events). Do not write
|
|
298
|
+
// those bytes yet: a video tool call has to be answered before Codex sees the
|
|
299
|
+
// turn, and a reset before that write is still safe to retry.
|
|
300
|
+
export async function readUntilTerminal(stream, output) {
|
|
301
|
+
const reader = stream.getReader();
|
|
302
|
+
const decoder = new TextDecoder();
|
|
303
|
+
let buffer = "";
|
|
304
|
+
const collected = [];
|
|
305
|
+
let released = false;
|
|
306
|
+
const finish = async (error) => {
|
|
307
|
+
if (released) return;
|
|
308
|
+
released = true;
|
|
309
|
+
await reader.cancel(error).catch(() => {});
|
|
310
|
+
};
|
|
311
|
+
try {
|
|
312
|
+
while (true) {
|
|
313
|
+
if (output?.destroyed || output?.writableEnded) throw clientClosed();
|
|
314
|
+
const { done, value } = await reader.read();
|
|
315
|
+
if (done) break;
|
|
316
|
+
buffer += decoder.decode(value, { stream: true });
|
|
317
|
+
const split = completeBlocks(buffer);
|
|
318
|
+
buffer = split.rest;
|
|
319
|
+
collected.push(...split.parts);
|
|
320
|
+
if (collected.some((part) => isTerminalSseBlock(part))) {
|
|
321
|
+
await finish();
|
|
322
|
+
return collected;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
buffer += decoder.decode();
|
|
326
|
+
if (buffer.trim()) collected.push(buffer);
|
|
327
|
+
if (!collected.some((part) => isTerminalSseBlock(part)))
|
|
328
|
+
throw prematureUpstreamClose();
|
|
329
|
+
await finish();
|
|
330
|
+
return collected;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
await finish(error);
|
|
333
|
+
if (error && typeof error === "object" && error.clientBytes == null)
|
|
334
|
+
error.clientBytes = 0;
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export async function readRelayProxySse(options, output, attempts = PROXY_ATTEMPTS) {
|
|
340
|
+
let parts;
|
|
341
|
+
await relayAttempts(
|
|
342
|
+
options,
|
|
343
|
+
async (response) => {
|
|
344
|
+
parts = await readUntilTerminal(response.body, output);
|
|
345
|
+
},
|
|
346
|
+
attempts,
|
|
347
|
+
);
|
|
348
|
+
return parts;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export async function emitProxySse(parts, output, map, usageBox, onClientByte) {
|
|
352
|
+
const rewrite = createSseRewriter(map);
|
|
353
|
+
let clientBytes = 0;
|
|
354
|
+
try {
|
|
355
|
+
for (const part of parts) {
|
|
356
|
+
const rewritten = rewrite(part);
|
|
357
|
+
if (rewritten === null) continue;
|
|
358
|
+
const block = `${rewritten}\n\n`;
|
|
359
|
+
if (output.destroyed || output.writableEnded) throw clientClosed();
|
|
360
|
+
const accepted = output.write(block);
|
|
361
|
+
clientBytes += Buffer.byteLength(block);
|
|
362
|
+
onClientByte?.();
|
|
363
|
+
if (!accepted) await drained(output);
|
|
364
|
+
}
|
|
365
|
+
} catch (error) {
|
|
366
|
+
if (error && typeof error === "object") error.clientBytes = clientBytes;
|
|
168
367
|
throw error;
|
|
169
368
|
} finally {
|
|
170
369
|
if (usageBox) usageBox.cacheUsage = rewrite.cacheUsage ?? null;
|
package/src/runtime.mjs
CHANGED
|
@@ -29,14 +29,11 @@ export async function startRuntime(options = {}) {
|
|
|
29
29
|
env_key: "CODEX_GROK_BRIDGE_TOKEN",
|
|
30
30
|
wire_api: "responses",
|
|
31
31
|
requires_openai_auth: false,
|
|
32
|
-
// The
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// turned every such reset into a dead turn.
|
|
38
|
-
request_max_retries: 2,
|
|
39
|
-
stream_max_retries: 2,
|
|
32
|
+
// The bridge buffers the upstream reply and retries a reset itself, up to
|
|
33
|
+
// two more attempts, before Codex has seen any SSE byte. Codex must not
|
|
34
|
+
// also resend the full prompt.
|
|
35
|
+
request_max_retries: 0,
|
|
36
|
+
stream_max_retries: 0,
|
|
40
37
|
// Do not inherit whatever Codex's default happens to be across upgrades.
|
|
41
38
|
stream_idle_timeout_ms: 300000,
|
|
42
39
|
};
|