ima2-gen 2.0.0 → 2.0.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 +150 -0
- package/README.md +12 -12
- package/bin/commands/backfillThumbs.js +24 -0
- package/bin/commands/edit.js +7 -6
- package/bin/commands/gen.js +13 -6
- package/bin/commands/multimode.js +5 -4
- package/bin/commands/node.js +4 -4
- package/bin/ima2.js +21 -11
- package/bin/lib/config-store.js +1 -1
- package/docs/API.md +184 -10
- package/docs/CLI.md +11 -4
- package/docs/FAQ.ko.md +16 -0
- package/docs/FAQ.md +30 -0
- package/docs/PROMPT_STUDIO.md +3 -1
- package/docs/README.ko.md +7 -3
- package/docs/migration/runtime-test-inventory.md +17 -1
- package/lib/agentImageVideoGen.js +261 -0
- package/lib/agentRuntime.js +11 -260
- package/lib/agentSettings.js +1 -1
- package/lib/agyImageAdapter.js +259 -0
- package/lib/capabilities.js +2 -1
- package/lib/configKeys.js +1 -1
- package/lib/errorClassify.js +8 -7
- package/lib/eventBus.js +71 -0
- package/lib/geminiApiImageAdapter.js +179 -0
- package/lib/generationErrors.js +3 -1
- package/lib/grokImageAdapter.js +74 -128
- package/lib/grokImageCore.js +153 -0
- package/lib/grokMultimodeAdapter.js +7 -4
- package/lib/grokRuntime.js +3 -0
- package/lib/grokSizeMapper.js +13 -1
- package/lib/grokVideoAdapter.js +14 -7
- package/lib/grokVideoCanvas.js +13 -0
- package/lib/grokVideoPlannerPrompt.js +53 -6
- package/lib/historyList.js +19 -2
- package/lib/imageModels.js +15 -0
- package/lib/imageThumb.js +38 -0
- package/lib/inflight.js +54 -17
- package/lib/multimodeHelpers.js +10 -0
- package/lib/nodeHelpers.js +59 -0
- package/lib/oauthProxy/prompts.js +30 -36
- package/lib/promptBuilder/systemPrompt.js +2 -5
- package/lib/promptSafetyPolicy.js +1 -5
- package/lib/providerOptions.js +36 -1
- package/lib/responsesFallback.js +53 -44
- package/lib/routeHelpers.js +44 -0
- package/lib/runtimeContext.js +27 -0
- package/lib/ssePublish.js +12 -0
- package/lib/storageMigration.js +1 -1
- package/lib/storyboardPrefix.js +28 -0
- package/lib/thumbBackfill.js +70 -0
- package/lib/vertexAuth.js +44 -0
- package/lib/videoThumb.js +60 -0
- package/package.json +7 -2
- package/routes/agy.js +44 -0
- package/routes/auth.js +242 -0
- package/routes/edit.js +48 -8
- package/routes/events.js +78 -0
- package/routes/generate.js +135 -135
- package/routes/history.js +13 -0
- package/routes/index.js +8 -0
- package/routes/keys.js +254 -0
- package/routes/multimode.js +138 -62
- package/routes/nodes.js +107 -129
- package/routes/quota.js +58 -7
- package/routes/video.js +107 -20
- package/server.js +123 -0
- package/skills/ima2/SKILL.md +98 -21
- package/ui/dist/.vite/manifest.json +12 -12
- package/ui/dist/assets/AgentWorkspace-Dth6YijN.js +3 -0
- package/ui/dist/assets/{CardNewsWorkspace-BN-ga1lG.js → CardNewsWorkspace-Dav3K5CT.js} +2 -2
- package/ui/dist/assets/{NodeCanvas-BbMa4IhI.js → NodeCanvas-C4ifFzB1.js} +2 -2
- package/ui/dist/assets/{PromptBuilderPanel-DRwBJRDQ.js → PromptBuilderPanel-CEcyU9PL.js} +1 -1
- package/ui/dist/assets/{PromptImportDialog-Dp85kHCq.js → PromptImportDialog-CgQ94Gth.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-BE8Q8MLD.js → PromptImportDiscoverySection-CuzyzbNI.js} +1 -1
- package/ui/dist/assets/{PromptImportFolderSection-PtH5x0sc.js → PromptImportFolderSection-DHLGlO6l.js} +1 -1
- package/ui/dist/assets/{PromptLibraryPanel-FnM9tHI9.js → PromptLibraryPanel-BOe18we8.js} +2 -2
- package/ui/dist/assets/SettingsWorkspace-Cdgnm4Wa.js +1 -0
- package/ui/dist/assets/index-C5PSahkr.js +1 -0
- package/ui/dist/assets/index-Dn2AhL6d.css +1 -0
- package/ui/dist/assets/index-Tjqx6wUV.js +23 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/AgentWorkspace-C21zqdTZ.js +0 -3
- package/ui/dist/assets/SettingsWorkspace-MARPGyBL.js +0 -1
- package/ui/dist/assets/index-BAFI6htx.js +0 -42
- package/ui/dist/assets/index-BSXxr_Bt.js +0 -1
- package/ui/dist/assets/index-DS-ADE7U.css +0 -1
package/routes/nodes.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir } from "fs/promises";
|
|
2
|
-
import { newNodeId, saveNode,
|
|
3
|
-
import { startJob, finishJob, registerJobAbortController, isJobCanceled } from "../lib/inflight.js";
|
|
2
|
+
import { newNodeId, saveNode, loadNodeMeta, loadAssetB64, } from "../lib/nodeStore.js";
|
|
3
|
+
import { startJob, finishJob, registerJobAbortController, isJobCanceled, isStartJobFailure, INFLIGHT_RETRY_AFTER_SECONDS } from "../lib/inflight.js";
|
|
4
4
|
import { isGenerationCanceledError, makeGenerationCanceledError, throwIfJobCanceled, } from "../lib/generationCancel.js";
|
|
5
5
|
import { detectImageMimeFromB64, summarizeReferencePayload, validateAndNormalizeRefs } from "../lib/refs.js";
|
|
6
6
|
import { classifyUpstreamError } from "../lib/errorClassify.js";
|
|
@@ -8,76 +8,22 @@ import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
|
|
|
8
8
|
import { resolveProviderOptions } from "../lib/providerOptions.js";
|
|
9
9
|
import { generateViaResponses, editViaResponses } from "../lib/responsesImageAdapter.js";
|
|
10
10
|
import { generateViaGrok } from "../lib/grokImageAdapter.js";
|
|
11
|
+
import { generateViaAgy } from "../lib/agyImageAdapter.js";
|
|
12
|
+
import { generateViaGeminiApi } from "../lib/geminiApiImageAdapter.js";
|
|
11
13
|
import { isNonRetryableGenerationError, normalizeGenerationFailure } from "../lib/generationErrors.js";
|
|
12
14
|
import { logEvent, logError } from "../lib/logger.js";
|
|
13
15
|
import { errInfo } from "../lib/errInfo.js";
|
|
14
16
|
import { requireRuntimeContext } from "../lib/runtimeContext.js";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (typeof moderation !== "string" || !ctx.config.oauth.validModeration.has(moderation)) {
|
|
20
|
-
return { error: "moderation must be one of: auto, low" };
|
|
21
|
-
}
|
|
22
|
-
return { moderation };
|
|
23
|
-
}
|
|
24
|
-
function wantsSse(req) {
|
|
25
|
-
const accept = typeof req.headers.accept === "string" ? req.headers.accept : "";
|
|
26
|
-
return accept.includes("text/event-stream");
|
|
27
|
-
}
|
|
28
|
-
function writeSse(res, event, data) {
|
|
29
|
-
res.write(`event: ${event}\n`);
|
|
30
|
-
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
31
|
-
}
|
|
32
|
-
function writeNodeError(res, status, code, message, parentNodeId, details = {}) {
|
|
33
|
-
if (res.headersSent) {
|
|
34
|
-
writeSse(res, "error", {
|
|
35
|
-
error: { code, message },
|
|
36
|
-
parentNodeId,
|
|
37
|
-
status,
|
|
38
|
-
...details,
|
|
39
|
-
});
|
|
40
|
-
res.end();
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
res.status(status).json({
|
|
44
|
-
error: { code, message },
|
|
45
|
-
parentNodeId,
|
|
46
|
-
status,
|
|
47
|
-
...details,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
function dataUrlFromB64(format, b64) {
|
|
51
|
-
return `data:image/${format === "jpeg" ? "jpeg" : format};base64,${b64}`;
|
|
52
|
-
}
|
|
53
|
-
function imageFormatFromMime(mime) {
|
|
54
|
-
if (mime === "image/jpeg")
|
|
55
|
-
return "jpeg";
|
|
56
|
-
if (mime === "image/webp")
|
|
57
|
-
return "webp";
|
|
58
|
-
return "png";
|
|
59
|
-
}
|
|
60
|
-
async function loadParentNodeB64(ctx, nodeId) {
|
|
61
|
-
for (const ext of ["png", "jpeg", "webp"]) {
|
|
62
|
-
const meta = await loadNodeMeta(ctx.rootDir, nodeId, ext, ctx.config.storage.generatedDir);
|
|
63
|
-
if (meta)
|
|
64
|
-
return loadNodeB64(ctx.rootDir, `${nodeId}.${ext}`, ctx.config.storage.generatedDir);
|
|
65
|
-
}
|
|
66
|
-
return loadNodeB64(ctx.rootDir, `${nodeId}.png`, ctx.config.storage.generatedDir);
|
|
67
|
-
}
|
|
68
|
-
function toGrokReferences(parentB64, refs) {
|
|
69
|
-
const parentMime = parentB64 ? detectImageMimeFromB64(parentB64) : null;
|
|
70
|
-
const parentRefs = parentB64
|
|
71
|
-
? [{ b64: parentB64, declaredMime: parentMime, detectedMime: parentMime }]
|
|
72
|
-
: [];
|
|
73
|
-
const normalizedRefs = refs.map((ref) => typeof ref === "string" ? { b64: ref } : ref);
|
|
74
|
-
return [...parentRefs, ...normalizedRefs];
|
|
75
|
-
}
|
|
17
|
+
import { validateModeration, imageFormatFromMime, writeSse, dataUrlFromB64 } from "../lib/routeHelpers.js";
|
|
18
|
+
import { publish } from "../lib/eventBus.js";
|
|
19
|
+
import { publishJobEvent } from "../lib/ssePublish.js";
|
|
20
|
+
import { asUpstream, wantsSse, writeNodeError, loadParentNodeB64, toGrokReferences, nodeErrorDetails, } from "../lib/nodeHelpers.js";
|
|
76
21
|
export function registerNodeRoutes(app, ctxRaw) {
|
|
77
22
|
const ctx = requireRuntimeContext(ctxRaw);
|
|
78
23
|
app.post("/api/node/generate", async (req, res) => {
|
|
79
24
|
const body = (req.body ?? {});
|
|
80
|
-
const
|
|
25
|
+
const asyncMode = body.async === true;
|
|
26
|
+
const streamResponse = !asyncMode && wantsSse(req);
|
|
81
27
|
const parentNodeId = (typeof body.parentNodeId === "string" ? body.parentNodeId : null);
|
|
82
28
|
const requestId = typeof body.requestId === "string" ? body.requestId : (req.id ?? "");
|
|
83
29
|
const sessionId = typeof body.sessionId === "string" ? body.sessionId : null;
|
|
@@ -89,21 +35,6 @@ export function registerNodeRoutes(app, ctxRaw) {
|
|
|
89
35
|
let finishCanceled = false;
|
|
90
36
|
const cancelController = new AbortController();
|
|
91
37
|
const referencePayload = summarizeReferencePayload(body.references);
|
|
92
|
-
startJob({
|
|
93
|
-
requestId,
|
|
94
|
-
kind: "node",
|
|
95
|
-
prompt: body.prompt,
|
|
96
|
-
meta: {
|
|
97
|
-
kind: "node",
|
|
98
|
-
sessionId,
|
|
99
|
-
parentNodeId,
|
|
100
|
-
clientNodeId,
|
|
101
|
-
refsCount: referencePayload.refsCount,
|
|
102
|
-
referenceBytes: referencePayload.referenceBytes,
|
|
103
|
-
referenceB64Chars: referencePayload.referenceB64Chars,
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
registerJobAbortController(requestId, cancelController);
|
|
107
38
|
try {
|
|
108
39
|
const { prompt, quality: rawQuality = "medium", size = "1024x1024", format = "png", moderation = "low", references = [], externalSrc = null, mode: promptMode = "auto", contextMode: rawContextMode = "parent-plus-refs", searchMode: rawSearchMode = "on", model: rawModel, reasoningEffort: rawReasoningEffort, } = body;
|
|
109
40
|
const { provider = "oauth" } = body;
|
|
@@ -135,7 +66,7 @@ export function registerNodeRoutes(app, ctxRaw) {
|
|
|
135
66
|
const effectiveSize = providerOptions.size;
|
|
136
67
|
const webSearchEnabled = providerOptions.webSearchEnabled;
|
|
137
68
|
const activeProvider = providerOptions.provider;
|
|
138
|
-
const effectiveImageModel = activeProvider === "grok" && quality === "high"
|
|
69
|
+
const effectiveImageModel = (activeProvider === "grok" || activeProvider === "grok-api") && quality === "high"
|
|
139
70
|
? "grok-imagine-image-quality"
|
|
140
71
|
: imageModel;
|
|
141
72
|
if (contextMode === "ancestry") {
|
|
@@ -193,19 +124,47 @@ export function registerNodeRoutes(app, ctxRaw) {
|
|
|
193
124
|
const refsForRequest = contextMode === "parent-only" ? [] : (refCheck.refDetails || refCheck.refs);
|
|
194
125
|
const parentImagePresent = !!parentB64;
|
|
195
126
|
const inputImageCount = (parentImagePresent ? 1 : 0) + refsForRequest.length;
|
|
196
|
-
if (activeProvider === "grok" && inputImageCount > 3) {
|
|
127
|
+
if ((activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api") && inputImageCount > 3) {
|
|
197
128
|
finishStatus = "error";
|
|
198
129
|
finishHttpStatus = 400;
|
|
199
|
-
|
|
130
|
+
const code = activeProvider === "agy" ? "AGY_REF_TOO_MANY" : "GROK_REF_TOO_MANY";
|
|
200
131
|
return res.status(400).json({
|
|
201
132
|
error: {
|
|
202
|
-
code
|
|
203
|
-
message: "Grok image editing supports up to 3 reference images
|
|
133
|
+
code,
|
|
134
|
+
message: `${activeProvider === "agy" ? "Agy" : "Grok"} image editing supports up to 3 reference images.`,
|
|
204
135
|
},
|
|
205
|
-
code
|
|
136
|
+
code,
|
|
206
137
|
parentNodeId,
|
|
207
138
|
});
|
|
208
139
|
}
|
|
140
|
+
const started = startJob({
|
|
141
|
+
requestId,
|
|
142
|
+
kind: "node",
|
|
143
|
+
prompt: body.prompt,
|
|
144
|
+
meta: {
|
|
145
|
+
kind: "node",
|
|
146
|
+
sessionId,
|
|
147
|
+
parentNodeId,
|
|
148
|
+
clientNodeId,
|
|
149
|
+
refsCount: referencePayload.refsCount,
|
|
150
|
+
referenceBytes: referencePayload.referenceBytes,
|
|
151
|
+
referenceB64Chars: referencePayload.referenceB64Chars,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
if (started && isStartJobFailure(started)) {
|
|
155
|
+
finishStatus = "error";
|
|
156
|
+
finishHttpStatus = started.code === "TOO_MANY_JOBS" ? 429 : 409;
|
|
157
|
+
finishErrorCode = started.code;
|
|
158
|
+
if (started.code === "TOO_MANY_JOBS") {
|
|
159
|
+
res.setHeader("Retry-After", String(INFLIGHT_RETRY_AFTER_SECONDS));
|
|
160
|
+
}
|
|
161
|
+
return writeNodeError(res, finishHttpStatus, started.code, started.code === "TOO_MANY_JOBS"
|
|
162
|
+
? "Too many concurrent generation jobs"
|
|
163
|
+
: "Request ID already in use", parentNodeId, {}, requestId);
|
|
164
|
+
}
|
|
165
|
+
registerJobAbortController(requestId, cancelController);
|
|
166
|
+
if (asyncMode)
|
|
167
|
+
res.status(202).json({ requestId });
|
|
209
168
|
logEvent("node", "request", {
|
|
210
169
|
requestId,
|
|
211
170
|
operation,
|
|
@@ -229,6 +188,7 @@ export function registerNodeRoutes(app, ctxRaw) {
|
|
|
229
188
|
promptChars: prompt.length,
|
|
230
189
|
promptMode: normalizedPromptMode,
|
|
231
190
|
});
|
|
191
|
+
const emitProgress = streamResponse || asyncMode;
|
|
232
192
|
if (streamResponse) {
|
|
233
193
|
res.writeHead(200, {
|
|
234
194
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
@@ -236,9 +196,14 @@ export function registerNodeRoutes(app, ctxRaw) {
|
|
|
236
196
|
Connection: "keep-alive",
|
|
237
197
|
});
|
|
238
198
|
writeSse(res, "phase", { requestId, phase: "streaming" });
|
|
199
|
+
publish(requestId, "phase", { requestId, phase: "streaming" });
|
|
200
|
+
}
|
|
201
|
+
else if (asyncMode) {
|
|
202
|
+
publish(requestId, "phase", { requestId, phase: "streaming" });
|
|
239
203
|
}
|
|
240
204
|
let b64, usage, webSearchCalls = 0, revisedPrompt = null;
|
|
241
|
-
|
|
205
|
+
const grokDirectApiKey = activeProvider === "grok-api" ? ctx.xaiApiKey : undefined;
|
|
206
|
+
let resultFormat = activeProvider === "grok" || activeProvider === "agy" || activeProvider === "grok-api" || activeProvider === "gemini-api" ? "jpeg" : format;
|
|
242
207
|
const MAX_RETRIES = 1;
|
|
243
208
|
let lastErr = null;
|
|
244
209
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
@@ -261,46 +226,66 @@ export function registerNodeRoutes(app, ctxRaw) {
|
|
|
261
226
|
searchMode,
|
|
262
227
|
webSearchEnabled,
|
|
263
228
|
});
|
|
264
|
-
const r = activeProvider === "
|
|
265
|
-
? await
|
|
229
|
+
const r = activeProvider === "gemini-api"
|
|
230
|
+
? await generateViaGeminiApi(parentB64 ? `Edit this image: ${prompt}` : prompt, requireRuntimeContext(ctx), {
|
|
266
231
|
model: effectiveImageModel,
|
|
267
232
|
size: effectiveSize,
|
|
268
|
-
requestId,
|
|
269
233
|
signal: cancelController.signal,
|
|
270
|
-
|
|
234
|
+
requestId,
|
|
235
|
+
references: parentB64
|
|
236
|
+
? [{ b64: parentB64, declaredMime: null, detectedMime: null }, ...(refCheck.refDetails || [])]
|
|
237
|
+
: refCheck.refDetails,
|
|
271
238
|
})
|
|
272
|
-
:
|
|
273
|
-
? await
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
reasoningEffort,
|
|
278
|
-
webSearchEnabled,
|
|
239
|
+
: activeProvider === "agy"
|
|
240
|
+
? await generateViaAgy(parentB64 ? `Edit this image: ${prompt}` : prompt, {
|
|
241
|
+
references: parentB64
|
|
242
|
+
? [{ b64: parentB64, declaredMime: null, detectedMime: null }]
|
|
243
|
+
: undefined,
|
|
279
244
|
signal: cancelController.signal,
|
|
245
|
+
requestId,
|
|
280
246
|
})
|
|
281
|
-
:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
247
|
+
: activeProvider === "grok" || activeProvider === "grok-api"
|
|
248
|
+
? await generateViaGrok(prompt, ctx, {
|
|
249
|
+
model: effectiveImageModel,
|
|
250
|
+
size: effectiveSize,
|
|
251
|
+
requestId,
|
|
252
|
+
signal: cancelController.signal,
|
|
253
|
+
references: toGrokReferences(parentB64, refsForRequest),
|
|
254
|
+
directApiKey: grokDirectApiKey,
|
|
255
|
+
})
|
|
256
|
+
: parentB64
|
|
257
|
+
? await editViaResponses(activeProvider, prompt, parentB64, quality, effectiveSize, moderation, normalizedPromptMode, ctx, requestId, {
|
|
258
|
+
model: effectiveImageModel,
|
|
259
|
+
references: refsForRequest,
|
|
260
|
+
searchMode,
|
|
261
|
+
reasoningEffort,
|
|
262
|
+
webSearchEnabled,
|
|
263
|
+
signal: cancelController.signal,
|
|
264
|
+
})
|
|
265
|
+
: await generateViaResponses(activeProvider, prompt, quality, effectiveSize, moderation, refsForRequest, requestId, normalizedPromptMode, ctx, {
|
|
266
|
+
model: effectiveImageModel,
|
|
267
|
+
reasoningEffort,
|
|
268
|
+
webSearchEnabled,
|
|
269
|
+
signal: cancelController.signal,
|
|
270
|
+
partialImages: emitProgress ? 2 : 0,
|
|
271
|
+
onPartialImage: emitProgress
|
|
272
|
+
? (partial) => {
|
|
273
|
+
if (isJobCanceled(requestId))
|
|
274
|
+
return;
|
|
275
|
+
const pd = { requestId, image: dataUrlFromB64(format, partial.b64), index: partial.index };
|
|
276
|
+
if (streamResponse)
|
|
277
|
+
writeSse(res, "partial", pd);
|
|
278
|
+
publish(requestId, "partial", pd);
|
|
279
|
+
}
|
|
280
|
+
: null,
|
|
281
|
+
});
|
|
297
282
|
throwIfJobCanceled(requestId);
|
|
298
283
|
if (r.b64) {
|
|
299
284
|
b64 = r.b64;
|
|
300
285
|
usage = r.usage;
|
|
301
286
|
webSearchCalls = r.webSearchCalls || 0;
|
|
302
287
|
revisedPrompt = r.revisedPrompt || null;
|
|
303
|
-
if (activeProvider === "grok") {
|
|
288
|
+
if (activeProvider === "grok" || activeProvider === "grok-api" || activeProvider === "gemini-api") {
|
|
304
289
|
resultFormat = imageFormatFromMime(("mime" in r ? r.mime : undefined) || detectImageMimeFromB64(r.b64) || "image/jpeg");
|
|
305
290
|
}
|
|
306
291
|
break;
|
|
@@ -346,18 +331,7 @@ export function registerNodeRoutes(app, ctxRaw) {
|
|
|
346
331
|
outerHttpAlreadyCommitted: res.headersSent,
|
|
347
332
|
sseErrorSent: streamResponse,
|
|
348
333
|
});
|
|
349
|
-
return writeNodeError(res, finishHttpStatus ?? 500, finishErrorCode ?? "NODE_GEN_FAILED", finalErr.message, parentNodeId,
|
|
350
|
-
upstreamCode: lastErr?.upstreamCode || lastErr?.code || null,
|
|
351
|
-
upstreamType: lastErr?.upstreamType || null,
|
|
352
|
-
upstreamParam: lastErr?.upstreamParam || null,
|
|
353
|
-
errorEventType: lastErr?.eventType || null,
|
|
354
|
-
errorEventCount: lastErr?.eventCount ?? null,
|
|
355
|
-
diagnosticReason: finalErr.diagnosticReason || lastErr?.diagnosticReason || null,
|
|
356
|
-
retryKind: finalErr.retryKind || lastErr?.retryKind || null,
|
|
357
|
-
referencesDroppedOnRetry: finalErr.referencesDroppedOnRetry ?? lastErr?.referencesDroppedOnRetry ?? null,
|
|
358
|
-
refsCount: finalErr.refsCount ?? lastErr?.refsCount ?? null,
|
|
359
|
-
inputImageCount: finalErr.inputImageCount ?? lastErr?.inputImageCount ?? null,
|
|
360
|
-
});
|
|
334
|
+
return writeNodeError(res, finishHttpStatus ?? 500, finishErrorCode ?? "NODE_GEN_FAILED", finalErr.message, parentNodeId, nodeErrorDetails(finalErr, lastErr), requestId);
|
|
361
335
|
}
|
|
362
336
|
const nodeId = newNodeId();
|
|
363
337
|
throwIfJobCanceled(requestId);
|
|
@@ -433,7 +407,11 @@ export function registerNodeRoutes(app, ctxRaw) {
|
|
|
433
407
|
revisedPrompt,
|
|
434
408
|
promptMode: normalizedPromptMode,
|
|
435
409
|
};
|
|
436
|
-
|
|
410
|
+
publishJobEvent(requestId, "done", payload);
|
|
411
|
+
if (res.writableEnded) {
|
|
412
|
+
// async mode — response already sent
|
|
413
|
+
}
|
|
414
|
+
else if (streamResponse) {
|
|
437
415
|
writeSse(res, "done", payload);
|
|
438
416
|
res.end();
|
|
439
417
|
}
|
|
@@ -450,7 +428,7 @@ export function registerNodeRoutes(app, ctxRaw) {
|
|
|
450
428
|
finishCanceled = true;
|
|
451
429
|
finishHttpStatus = canceled.status;
|
|
452
430
|
finishErrorCode = canceled.code;
|
|
453
|
-
return writeNodeError(res, canceled.status, canceled.code, canceled.message, parentNodeId);
|
|
431
|
+
return writeNodeError(res, canceled.status, canceled.code, canceled.message, parentNodeId, {}, requestId);
|
|
454
432
|
}
|
|
455
433
|
finishStatus = "error";
|
|
456
434
|
finishHttpStatus = err.status || 500;
|
|
@@ -460,7 +438,7 @@ export function registerNodeRoutes(app, ctxRaw) {
|
|
|
460
438
|
upstreamCode: ext.upstreamCode || null,
|
|
461
439
|
upstreamType: ext.upstreamType || null,
|
|
462
440
|
upstreamParam: ext.upstreamParam || null,
|
|
463
|
-
});
|
|
441
|
+
}, requestId);
|
|
464
442
|
}
|
|
465
443
|
finally {
|
|
466
444
|
finishJob(requestId, {
|
package/routes/quota.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
function readCodexTokens() {
|
|
@@ -53,14 +53,65 @@ async function fetchCodexUsage(tokens) {
|
|
|
53
53
|
return { provider: "codex", error: true, windows: [] };
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
function grokTierFromLimit(val) {
|
|
57
|
+
if (val >= 150_000)
|
|
58
|
+
return "SuperGrok Heavy";
|
|
59
|
+
if (val >= 15_000)
|
|
60
|
+
return "SuperGrok";
|
|
61
|
+
return `SuperGrok (${val} val)`;
|
|
62
|
+
}
|
|
63
|
+
async function fetchGrokBilling() {
|
|
64
|
+
try {
|
|
65
|
+
const authPath = join(homedir(), ".progrok", "auth.json");
|
|
66
|
+
if (!existsSync(authPath))
|
|
67
|
+
return { provider: "grok", authenticated: false, windows: [] };
|
|
68
|
+
const auth = JSON.parse(readFileSync(authPath, "utf8"));
|
|
69
|
+
if (!auth.accessToken)
|
|
70
|
+
return { provider: "grok", authenticated: false, windows: [] };
|
|
71
|
+
const headers = { Authorization: `Bearer ${auth.accessToken}` };
|
|
72
|
+
const [billingRes, userRes] = await Promise.allSettled([
|
|
73
|
+
fetch("https://cli-chat-proxy.grok.com/v1/billing", { headers, signal: AbortSignal.timeout(8000) }),
|
|
74
|
+
fetch("https://cli-chat-proxy.grok.com/v1/user", { headers, signal: AbortSignal.timeout(5000) }),
|
|
75
|
+
]);
|
|
76
|
+
if (billingRes.status !== "fulfilled" || !billingRes.value.ok) {
|
|
77
|
+
return { provider: "grok", authenticated: true, windows: [] };
|
|
78
|
+
}
|
|
79
|
+
const billing = (await billingRes.value.json()).config;
|
|
80
|
+
const limit = billing.monthlyLimit.val;
|
|
81
|
+
const used = billing.used.val;
|
|
82
|
+
let email = null;
|
|
83
|
+
if (userRes.status === "fulfilled" && userRes.value.ok) {
|
|
84
|
+
const user = await userRes.value.json();
|
|
85
|
+
email = user.email ?? null;
|
|
86
|
+
}
|
|
87
|
+
const tier = grokTierFromLimit(limit);
|
|
88
|
+
return {
|
|
89
|
+
provider: "grok",
|
|
90
|
+
account: { email, plan: tier },
|
|
91
|
+
windows: [{
|
|
92
|
+
label: "monthly",
|
|
93
|
+
percent: limit > 0 ? Math.round((used / limit) * 100) : 0,
|
|
94
|
+
resetsAt: billing.billingPeriodEnd,
|
|
95
|
+
}],
|
|
96
|
+
billing: { usedUsd: used / 100, limitUsd: limit / 100 },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return { provider: "grok", error: true, windows: [] };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
56
103
|
export function registerQuotaRoutes(app, _ctx) {
|
|
57
104
|
app.get("/api/quota", async (_req, res) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
105
|
+
try {
|
|
106
|
+
const tokens = readCodexTokens();
|
|
107
|
+
const [codex, grok] = await Promise.all([
|
|
108
|
+
tokens ? fetchCodexUsage(tokens) : Promise.resolve({ provider: "codex", authenticated: false, windows: [] }),
|
|
109
|
+
fetchGrokBilling(),
|
|
110
|
+
]);
|
|
111
|
+
res.json({ codex, grok });
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
res.status(500).json({ error: "Failed to fetch quota" });
|
|
62
115
|
}
|
|
63
|
-
const codex = await fetchCodexUsage(tokens);
|
|
64
|
-
res.json({ codex });
|
|
65
116
|
});
|
|
66
117
|
}
|