ima2-gen 1.1.20 → 1.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +15 -25
  2. package/bin/commands/capabilities.js +2 -2
  3. package/bin/commands/capabilities.ts +2 -2
  4. package/bin/commands/defaults.js +2 -2
  5. package/bin/commands/defaults.ts +2 -2
  6. package/bin/commands/doctor.js +3 -3
  7. package/bin/commands/doctor.ts +3 -3
  8. package/bin/commands/edit.js +1 -1
  9. package/bin/commands/edit.ts +1 -1
  10. package/bin/commands/gen.js +1 -1
  11. package/bin/commands/gen.ts +1 -1
  12. package/bin/commands/grok.js +16 -11
  13. package/bin/commands/grok.ts +16 -11
  14. package/bin/commands/multimode.js +1 -1
  15. package/bin/commands/multimode.ts +1 -1
  16. package/bin/commands/observability.js +2 -2
  17. package/bin/commands/observability.ts +2 -2
  18. package/bin/commands/video.js +335 -13
  19. package/bin/commands/video.ts +249 -12
  20. package/bin/ima2.js +9 -9
  21. package/bin/ima2.ts +9 -9
  22. package/bin/lib/error-hints.js +2 -2
  23. package/bin/lib/error-hints.ts +2 -2
  24. package/docs/API.md +112 -3
  25. package/docs/CLI.md +61 -7
  26. package/docs/FAQ.ko.md +15 -20
  27. package/docs/FAQ.md +14 -19
  28. package/docs/NPX_QUICKSTART.md +40 -0
  29. package/docs/PROMPT_STUDIO.ko.md +1 -1
  30. package/docs/PROMPT_STUDIO.md +1 -1
  31. package/docs/README.ja.md +6 -16
  32. package/docs/README.ko.md +10 -20
  33. package/docs/README.zh-CN.md +7 -17
  34. package/docs/migration/runtime-test-inventory.md +8 -1
  35. package/lib/agentRuntime.js +19 -5
  36. package/lib/agentRuntime.ts +17 -5
  37. package/lib/capabilities.js +1 -1
  38. package/lib/capabilities.ts +1 -1
  39. package/lib/generationErrors.js +1 -1
  40. package/lib/generationErrors.ts +1 -1
  41. package/lib/grokProxyLauncher.js +26 -3
  42. package/lib/grokProxyLauncher.ts +27 -3
  43. package/lib/grokVideoAdapter.js +18 -89
  44. package/lib/grokVideoAdapter.ts +27 -88
  45. package/lib/grokVideoCanvas.js +25 -0
  46. package/lib/grokVideoCanvas.ts +26 -0
  47. package/lib/grokVideoDownload.js +58 -0
  48. package/lib/grokVideoDownload.ts +59 -0
  49. package/lib/grokVideoPlannerPrompt.js +64 -0
  50. package/lib/grokVideoPlannerPrompt.ts +67 -0
  51. package/lib/historyList.js +7 -1
  52. package/lib/historyList.ts +5 -1
  53. package/lib/oauthLauncher.js +21 -6
  54. package/lib/oauthLauncher.ts +22 -6
  55. package/lib/videoContinuity.js +149 -0
  56. package/lib/videoContinuity.ts +180 -0
  57. package/lib/videoFrameExtract.js +80 -0
  58. package/lib/videoFrameExtract.ts +78 -0
  59. package/node_modules/progrok/dist/index.js +187 -88
  60. package/node_modules/progrok/dist/index.js.map +1 -1
  61. package/node_modules/progrok/package.json +1 -1
  62. package/node_modules/progrok/skills/progrok/SKILL.md +33 -4
  63. package/package.json +2 -2
  64. package/routes/index.js +4 -0
  65. package/routes/index.ts +4 -0
  66. package/routes/quota.js +66 -0
  67. package/routes/quota.ts +89 -0
  68. package/routes/video.js +77 -15
  69. package/routes/video.ts +82 -14
  70. package/routes/videoExtended.js +293 -0
  71. package/routes/videoExtended.ts +284 -0
  72. package/server.js +6 -2
  73. package/server.ts +5 -2
  74. package/skills/ima2/SKILL.md +320 -7
  75. package/ui/dist/.vite/manifest.json +12 -12
  76. package/ui/dist/assets/{AgentWorkspace-DS8uvoLI.js → AgentWorkspace-B_hq9CLg.js} +2 -2
  77. package/ui/dist/assets/{CardNewsWorkspace-CYxMsE67.js → CardNewsWorkspace-wD12J7qk.js} +1 -1
  78. package/ui/dist/assets/{NodeCanvas-DccIc347.js → NodeCanvas-CI_wuPMf.js} +1 -1
  79. package/ui/dist/assets/{PromptBuilderPanel-BvxxwSJp.js → PromptBuilderPanel-CUTujJUV.js} +1 -1
  80. package/ui/dist/assets/{PromptImportDialog-u1_BFDRd.js → PromptImportDialog-CUi66jPK.js} +2 -2
  81. package/ui/dist/assets/{PromptImportDiscoverySection-C5uvkVSz.js → PromptImportDiscoverySection-Cm3vrjY4.js} +1 -1
  82. package/ui/dist/assets/{PromptImportFolderSection-D3E_O1SD.js → PromptImportFolderSection-DOtWTD9n.js} +1 -1
  83. package/ui/dist/assets/{PromptLibraryPanel-4gyf9CB9.js → PromptLibraryPanel-BMjQegRa.js} +2 -2
  84. package/ui/dist/assets/SettingsWorkspace-PiaVnsdA.js +1 -0
  85. package/ui/dist/assets/{index-DoKtXbod.js → index-31uVIdt4.js} +1 -1
  86. package/ui/dist/assets/index-CjgnNtgt.css +1 -0
  87. package/ui/dist/assets/index-Da2s4_-5.js +36 -0
  88. package/ui/dist/index.html +2 -2
  89. package/vendor/progrok-0.2.0.tgz +0 -0
  90. package/ui/dist/assets/SettingsWorkspace-F3eNu3mJ.js +0 -1
  91. package/ui/dist/assets/index-B6tcw_UF.css +0 -1
  92. package/ui/dist/assets/index-DYOh6gQD.js +0 -32
  93. package/vendor/progrok-0.1.1.tgz +0 -0
@@ -0,0 +1,284 @@
1
+ import type { Express, Request, Response } from "express";
2
+ import { basename, join } from "node:path";
3
+ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
4
+ import { randomBytes } from "node:crypto";
5
+ import type { RouteRuntimeContext, RuntimeContext } from "../lib/runtimeContext.js";
6
+ import { requireRuntimeContext } from "../lib/runtimeContext.js";
7
+ import { getGrokProxyUrl } from "../lib/grokRuntime.js";
8
+ import { logEvent, logError } from "../lib/logger.js";
9
+ import { downloadVideo, pollVideoUntilDone } from "../lib/grokVideoAdapter.js";
10
+ import { invalidateHistoryIndex } from "../lib/historyIndex.js";
11
+ import { ACTIVE_VIDEO_PROMPT_GUIDANCE, appendVideoContinuityEntry, lineageFromVideoMetadata, readVideoSidecar } from "../lib/videoContinuity.js";
12
+ import { assertLocalMp4, extractVideoFrame, safeGeneratedFilePath } from "../lib/videoFrameExtract.js";
13
+
14
+ function videoProxyUrl(ctx: RuntimeContext, path: string) {
15
+ return { url: getGrokProxyUrl(ctx, path), headers: { "Content-Type": "application/json", Authorization: "Bearer dummy" } };
16
+ }
17
+
18
+ function routeError(message: string, status = 400): Error & { status: number } {
19
+ return Object.assign(new Error(message), { status });
20
+ }
21
+
22
+ function sendError(res: Response, err: any): void {
23
+ res.status(typeof err?.status === "number" ? err.status : 500).json({ error: err?.message || String(err) });
24
+ }
25
+
26
+ async function safeGeneratedFile(ctx: RuntimeContext, file: string, options: { requireMp4?: boolean } = {}): Promise<string> {
27
+ return safeGeneratedFilePath(ctx.config.storage.generatedDir, file, options);
28
+ }
29
+
30
+ function summarizeSource(input: string): Record<string, unknown> {
31
+ if (input.startsWith("data:video/")) {
32
+ const encoded = input.split(",", 2)[1] || "";
33
+ return { kind: "data-url", approximateBytes: Math.floor(encoded.length * 0.75) };
34
+ }
35
+ if (/^https?:\/\//i.test(input)) {
36
+ try {
37
+ const parsed = new URL(input);
38
+ return { kind: "url", origin: parsed.origin, pathname: basename(parsed.pathname) };
39
+ } catch {
40
+ return { kind: "url" };
41
+ }
42
+ }
43
+ if (/^file[-_][A-Za-z0-9._-]+$/.test(input)) return { kind: "file_id" };
44
+ return { kind: "generated-file", filename: basename(input) };
45
+ }
46
+
47
+ async function resolveVideoInput(ctx: RuntimeContext, input: string): Promise<Record<string, string>> {
48
+ if (/^https?:\/\//i.test(input) || input.startsWith("data:video/")) return { url: input };
49
+ if (/^file[-_][A-Za-z0-9._-]+$/.test(input)) return { file_id: input };
50
+ const inputPath = await safeGeneratedFile(ctx, input, { requireMp4: true });
51
+ await assertLocalMp4(inputPath);
52
+ const buf = await readFile(inputPath);
53
+ return { url: `data:video/mp4;base64,${buf.toString("base64")}` };
54
+ }
55
+
56
+ function validateEditModel(model: unknown): string {
57
+ if (typeof model !== "string") throw routeError("model must be a string", 400);
58
+ if (model !== "grok-imagine-video") throw routeError("Video edit/extension only supports grok-imagine-video", 400);
59
+ return model;
60
+ }
61
+
62
+ async function saveVideoResult(
63
+ ctx: RuntimeContext,
64
+ options: { requestId: string; prompt: string; model: string; operation: "edit" | "extend"; source: string; duration: number | null; videoUrl: string; usage?: Record<string, number> | null; signal?: AbortSignal },
65
+ ): Promise<{ filename: string; url: string; sourceUrl: string }> {
66
+ const { buffer, contentType } = await downloadVideo(ctx, options.videoUrl, options.signal);
67
+ await mkdir(ctx.config.storage.generatedDir, { recursive: true });
68
+ const rand = randomBytes(ctx.config.ids.generatedHexBytes).toString("hex");
69
+ const filename = `${Date.now()}_${rand}.mp4`;
70
+ const filePath = join(ctx.config.storage.generatedDir, filename);
71
+ const sourceFilename = /^https?:\/\//i.test(options.source) || options.source.startsWith("data:") || /^file[-_]/.test(options.source)
72
+ ? null
73
+ : basename(options.source);
74
+ const sourceMeta = sourceFilename ? await readVideoSidecar(ctx.config.storage.generatedDir, sourceFilename) : null;
75
+ const parentLineage = sourceFilename ? lineageFromVideoMetadata(sourceFilename, sourceMeta) : null;
76
+ const videoContinuity = appendVideoContinuityEntry(parentLineage, {
77
+ filename,
78
+ userPrompt: options.prompt,
79
+ revisedPrompt: options.prompt,
80
+ createdAt: Date.now(),
81
+ });
82
+ await writeFile(filePath, buffer);
83
+ try {
84
+ await writeFile(
85
+ `${filePath}.json`,
86
+ JSON.stringify({
87
+ kind: "video",
88
+ mediaType: "video",
89
+ requestId: options.requestId,
90
+ prompt: options.prompt,
91
+ userPrompt: options.prompt,
92
+ provider: "grok",
93
+ model: options.model,
94
+ createdAt: Date.now(),
95
+ usage: options.usage ?? null,
96
+ revisedPrompt: options.prompt,
97
+ videoContinuity,
98
+ video: {
99
+ operation: options.operation,
100
+ duration: options.duration,
101
+ source: summarizeSource(options.source),
102
+ sourceUrl: summarizeSource(options.videoUrl),
103
+ contentType,
104
+ },
105
+ }),
106
+ );
107
+ } catch (err) {
108
+ await unlink(filePath).catch(() => {});
109
+ throw err;
110
+ }
111
+ invalidateHistoryIndex();
112
+ return { filename, url: `/generated/${encodeURIComponent(filename)}`, sourceUrl: options.videoUrl };
113
+ }
114
+
115
+ function requestSignal(req: Request, res: Response): AbortSignal {
116
+ const ac = new AbortController();
117
+ const abort = () => {
118
+ if (!res.writableEnded) ac.abort();
119
+ };
120
+ req.on("aborted", abort);
121
+ res.on("close", abort);
122
+ return ac.signal;
123
+ }
124
+
125
+ function requirePrompt(value: unknown): string | null {
126
+ return typeof value === "string" && value.trim() ? value : null;
127
+ }
128
+
129
+ function extractOutputText(data: Record<string, unknown>): string {
130
+ const output = Array.isArray(data.output) ? data.output : [];
131
+ const texts: string[] = [];
132
+ for (const item of output) {
133
+ const content = (item as any)?.content;
134
+ if (!Array.isArray(content)) continue;
135
+ for (const part of content) {
136
+ if (part?.type === "output_text" && typeof part.text === "string") texts.push(part.text);
137
+ if (part?.type === "text" && typeof part.text === "string") texts.push(part.text);
138
+ }
139
+ }
140
+ return texts.join("\n").trim();
141
+ }
142
+
143
+ export function registerVideoExtendedRoutes(app: Express, ctxRaw: RouteRuntimeContext) {
144
+ const ctx = requireRuntimeContext(ctxRaw);
145
+
146
+ // --- Video Edit (V2V) ---
147
+ app.post("/api/video/edit", async (req: Request, res: Response) => {
148
+ try {
149
+ const { prompt: rawPrompt, videoUrl, model = "grok-imagine-video" } = req.body ?? {};
150
+ const prompt = requirePrompt(rawPrompt);
151
+ if (!prompt) return res.status(400).json({ error: "prompt required", code: "PROMPT_REQUIRED", guidance: ACTIVE_VIDEO_PROMPT_GUIDANCE });
152
+ if (!videoUrl || typeof videoUrl !== "string") return res.status(400).json({ error: "videoUrl required" });
153
+ const validModel = validateEditModel(model);
154
+ const signal = requestSignal(req, res);
155
+
156
+ const { url, headers } = videoProxyUrl(ctx, "/v1/videos/edits");
157
+ const video = await resolveVideoInput(ctx, videoUrl);
158
+ const apiRes = await fetch(url, { method: "POST", headers, body: JSON.stringify({ model: validModel, prompt, video }), signal });
159
+ if (!apiRes.ok) { const t = await apiRes.text(); return res.status(apiRes.status).json({ error: t }); }
160
+ const { request_id } = (await apiRes.json()) as { request_id: string };
161
+ if (!request_id) return res.status(502).json({ error: "No request_id in response" });
162
+ logEvent("video", "edit:start", { requestId: request_id, model: validModel });
163
+
164
+ const result = await pollVideoUntilDone(ctx, request_id, { signal });
165
+ if (result.respectModeration === false) return res.status(502).json({ error: "Grok video blocked by moderation" });
166
+ if (!result.videoUrl) return res.status(502).json({ error: "No video URL in response" });
167
+ const saved = await saveVideoResult(ctx, { requestId: request_id, prompt, model: validModel, operation: "edit", source: videoUrl, duration: result.duration ?? null, videoUrl: result.videoUrl, usage: result.usage, signal });
168
+
169
+ logEvent("video", "edit:done", { requestId: request_id });
170
+ res.json({ requestId: request_id, url: saved.url, filename: saved.filename, sourceUrl: saved.sourceUrl, duration: result.duration, model: validModel });
171
+ } catch (err: any) {
172
+ logError("video", "edit:error", err);
173
+ sendError(res, err);
174
+ }
175
+ });
176
+
177
+ // --- Video Extension ---
178
+ app.post("/api/video/extend", async (req: Request, res: Response) => {
179
+ try {
180
+ const { prompt: rawPrompt, videoUrl, duration = 6, model = "grok-imagine-video" } = req.body ?? {};
181
+ const prompt = requirePrompt(rawPrompt);
182
+ if (!prompt) return res.status(400).json({ error: "prompt required", code: "PROMPT_REQUIRED", guidance: ACTIVE_VIDEO_PROMPT_GUIDANCE });
183
+ if (!videoUrl || typeof videoUrl !== "string") return res.status(400).json({ error: "videoUrl required" });
184
+ const validModel = validateEditModel(model);
185
+ const dur = Number(duration);
186
+ if (!Number.isInteger(dur) || dur < 2 || dur > 10) return res.status(400).json({ error: "duration must be an integer between 2 and 10" });
187
+ const signal = requestSignal(req, res);
188
+
189
+ const { url, headers } = videoProxyUrl(ctx, "/v1/videos/extensions");
190
+ const video = await resolveVideoInput(ctx, videoUrl);
191
+ const apiRes = await fetch(url, { method: "POST", headers, body: JSON.stringify({ model: validModel, prompt, duration: dur, video }), signal });
192
+ if (!apiRes.ok) { const t = await apiRes.text(); return res.status(apiRes.status).json({ error: t }); }
193
+ const { request_id } = (await apiRes.json()) as { request_id: string };
194
+ if (!request_id) return res.status(502).json({ error: "No request_id in response" });
195
+ logEvent("video", "extend:start", { requestId: request_id, model: validModel, duration: dur });
196
+
197
+ const result = await pollVideoUntilDone(ctx, request_id, { signal });
198
+ if (result.respectModeration === false) return res.status(502).json({ error: "Grok video blocked by moderation" });
199
+ if (!result.videoUrl) return res.status(502).json({ error: "No video URL in response" });
200
+ const saved = await saveVideoResult(ctx, { requestId: request_id, prompt, model: validModel, operation: "extend", source: videoUrl, duration: result.duration ?? null, videoUrl: result.videoUrl, usage: result.usage, signal });
201
+
202
+ logEvent("video", "extend:done", { requestId: request_id, totalDuration: result.duration });
203
+ res.json({ requestId: request_id, url: saved.url, filename: saved.filename, sourceUrl: saved.sourceUrl, duration: result.duration, model: validModel });
204
+ } catch (err: any) {
205
+ logError("video", "extend:error", err);
206
+ sendError(res, err);
207
+ }
208
+ });
209
+
210
+ // --- Video Frame Extraction ---
211
+ app.get("/api/video/frame", async (req: Request, res: Response) => {
212
+ try {
213
+ const file = req.query.file as string | undefined;
214
+ const position = (req.query.position as string) || "last";
215
+ if (!file) return res.status(400).json({ error: "file query param required" });
216
+ const inputPath = await safeGeneratedFile(ctx, file, { requireMp4: true });
217
+ await assertLocalMp4(inputPath);
218
+
219
+ const tmpOut = join(ctx.config.storage.generatedDir, `frame_tmp_${randomBytes(4).toString("hex")}.png`);
220
+ try {
221
+ await extractVideoFrame(inputPath, tmpOut, position);
222
+ const frame = await readFile(tmpOut);
223
+ res.type("png").send(frame);
224
+ } catch (err: any) {
225
+ return res.status(500).json({ error: "ffmpeg failed" });
226
+ } finally {
227
+ await unlink(tmpOut).catch(() => {});
228
+ }
229
+ } catch (err: any) {
230
+ logError("video", "frame:error", err);
231
+ sendError(res, err);
232
+ }
233
+ });
234
+
235
+ // --- Video Analysis (Grok 4.3 Vision) ---
236
+ app.post("/api/video/analyze", async (req: Request, res: Response) => {
237
+ try {
238
+ const { videoUrl } = req.body ?? {};
239
+ if (!videoUrl || typeof videoUrl !== "string") return res.status(400).json({ error: "videoUrl required" });
240
+ if (/^https?:\/\//i.test(videoUrl) || videoUrl.startsWith("data:")) {
241
+ return res.status(400).json({ error: "videoUrl must be a generated .mp4 filename" });
242
+ }
243
+ const input = await safeGeneratedFile(ctx, videoUrl, { requireMp4: true });
244
+ await assertLocalMp4(input);
245
+ const firstFrame = join(ctx.config.storage.generatedDir, `analyze_first_${randomBytes(4).toString("hex")}.png`);
246
+ const lastFrame = join(ctx.config.storage.generatedDir, `analyze_last_${randomBytes(4).toString("hex")}.png`);
247
+
248
+ try {
249
+ await extractVideoFrame(input, firstFrame, "0");
250
+ await extractVideoFrame(input, lastFrame, "last");
251
+ const first = (await readFile(firstFrame)).toString("base64");
252
+ const last = (await readFile(lastFrame)).toString("base64");
253
+ const { url, headers } = videoProxyUrl(ctx, "/v1/responses");
254
+ const apiRes = await fetch(url, {
255
+ method: "POST",
256
+ headers,
257
+ body: JSON.stringify({
258
+ model: "grok-4.3",
259
+ input: [{
260
+ role: "user",
261
+ content: [
262
+ { type: "input_image", image_url: `data:image/png;base64,${first}`, detail: "high" },
263
+ { type: "input_image", image_url: `data:image/png;base64,${last}`, detail: "high" },
264
+ { type: "input_text", text: "Analyze these first and last frames from a video for recreation. Infer likely motion between them. Include shot type, camera movement, lighting, color palette, subjects, motion direction/speed, mood, and audio/sound prompt suggestions. Be specific and cinematic." },
265
+ ],
266
+ }],
267
+ }),
268
+ });
269
+ if (!apiRes.ok) { const t = await apiRes.text(); return res.status(apiRes.status).json({ error: t }); }
270
+ const data = (await apiRes.json()) as Record<string, unknown>;
271
+ const text = extractOutputText(data);
272
+ if (!text) return res.status(502).json({ error: "No analysis text in response" });
273
+ logEvent("video", "analyze:done", { videoUrl, chars: text.length });
274
+ res.json({ analysis: text, model: "grok-4.3", method: "first-last-frame" });
275
+ } finally {
276
+ await unlink(firstFrame).catch(() => {});
277
+ await unlink(lastFrame).catch(() => {});
278
+ }
279
+ } catch (err: any) {
280
+ logError("video", "analyze:error", err);
281
+ sendError(res, err);
282
+ }
283
+ });
284
+ }
package/server.js CHANGED
@@ -74,7 +74,11 @@ export function buildApp(ctx) {
74
74
  app.use("/assets", (_req, res) => {
75
75
  res.status(404).type("text/plain").send("Asset not found");
76
76
  });
77
- app.use("/generated", express.static(ctx.config.storage.generatedDir, {
77
+ app.use("/generated", (req, res, next) => {
78
+ if (req.path.endsWith(".json"))
79
+ return res.status(404).type("text/plain").send("Generated metadata is not public");
80
+ return next();
81
+ }, express.static(ctx.config.storage.generatedDir, {
78
82
  maxAge: ctx.config.storage.staticMaxAge,
79
83
  immutable: true,
80
84
  }));
@@ -254,7 +258,7 @@ export async function startServer(overrides = {}) {
254
258
  ctx.serverActualPort = getServerPort(server) || ctx.config.server.port;
255
259
  ctx.serverUrl = `http://${runtimeHostUrl(ctx.config.server.host)}:${ctx.serverActualPort}`;
256
260
  console.log(`Image Gen running at ${ctx.serverUrl}`);
257
- console.log(`Provider policy: OAuth, API-key Responses, and Grok Images providers. OAuth proxy port ${ctx.oauthPort}; Grok proxy port ${ctx.grokActualPort || ctx.grokPort}.`);
261
+ console.log(`Provider policy: GPT OAuth, API-key Responses, and Grok Images providers. GPT OAuth proxy port ${ctx.oauthPort}; Grok proxy port ${ctx.grokActualPort || ctx.grokPort}.`);
258
262
  advertise(ctx);
259
263
  try {
260
264
  const s = ensureDefaultSession();
package/server.ts CHANGED
@@ -92,7 +92,10 @@ export function buildApp(ctx: RuntimeContext) {
92
92
  app.use("/assets", (_req, res) => {
93
93
  res.status(404).type("text/plain").send("Asset not found");
94
94
  });
95
- app.use("/generated", express.static(ctx.config.storage.generatedDir, {
95
+ app.use("/generated", (req, res, next) => {
96
+ if (req.path.endsWith(".json")) return res.status(404).type("text/plain").send("Generated metadata is not public");
97
+ return next();
98
+ }, express.static(ctx.config.storage.generatedDir, {
96
99
  maxAge: ctx.config.storage.staticMaxAge,
97
100
  immutable: true,
98
101
  }));
@@ -267,7 +270,7 @@ export async function startServer(overrides: StartServerOverrides = {}) {
267
270
  ctx.serverActualPort = getServerPort(server) || ctx.config.server.port;
268
271
  ctx.serverUrl = `http://${runtimeHostUrl(ctx.config.server.host)}:${ctx.serverActualPort}`;
269
272
  console.log(`Image Gen running at ${ctx.serverUrl}`);
270
- console.log(`Provider policy: OAuth, API-key Responses, and Grok Images providers. OAuth proxy port ${ctx.oauthPort}; Grok proxy port ${ctx.grokActualPort || ctx.grokPort}.`);
273
+ console.log(`Provider policy: GPT OAuth, API-key Responses, and Grok Images providers. GPT OAuth proxy port ${ctx.oauthPort}; Grok proxy port ${ctx.grokActualPort || ctx.grokPort}.`);
271
274
  advertise(ctx);
272
275
  try {
273
276
  const s = ensureDefaultSession();