@dorely13/ai-imagegen-cli 0.2.0 → 0.3.0
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/help.js +5 -3
- package/dist/index.js +392 -401
- package/dist/index.js.map +1 -1
- package/dist/usage-log.js +39 -0
- package/package.json +45 -45
- package/skill/SKILL.md +18 -4
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { mkdir as
|
|
5
|
-
import { basename, dirname as
|
|
4
|
+
import { mkdir as mkdir5, readFile as readFile3, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname as dirname5, extname, join as join5, resolve as resolve3 } from "node:path";
|
|
6
6
|
|
|
7
7
|
// ../../packages/core/dist/types.js
|
|
8
8
|
var ProviderError = class extends Error {
|
|
@@ -226,20 +226,24 @@ var AUTH_ENDPOINT = "https://auth.openai.com/oauth/authorize";
|
|
|
226
226
|
var TOKEN_ENDPOINT = "https://auth.openai.com/oauth/token";
|
|
227
227
|
var CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
228
228
|
var SCOPE = "openid profile email offline_access";
|
|
229
|
-
var
|
|
229
|
+
var IMAGES_ENDPOINT = "https://chatgpt.com/backend-api/codex/images";
|
|
230
230
|
var DEFAULT_REDIRECT_URI = "http://localhost:1455/auth/callback";
|
|
231
231
|
var LOOPBACK_PORT = 1455;
|
|
232
232
|
var CALLBACK_PATH = "/auth/callback";
|
|
233
233
|
var DEFAULT_IMAGE_MODEL = "gpt-image-2.5-flare";
|
|
234
|
-
var DEFAULT_ORCHESTRATION_MODEL = "gpt-5.6-luna";
|
|
235
234
|
var DEFAULT_QUALITY = "auto";
|
|
236
235
|
var REQUEST_TIMEOUT_SECONDS = 600;
|
|
237
|
-
var PARTIAL_IMAGES = 2;
|
|
238
236
|
var MAX_REFERENCE_IMAGES2 = 4;
|
|
239
237
|
var MAX_IMAGE_COUNT2 = 4;
|
|
240
238
|
var MIN_IMAGE_COUNT2 = 1;
|
|
241
239
|
var FLOW_TIMEOUT_MINUTES = 10;
|
|
242
240
|
var USER_AGENT = "ImageGenCli";
|
|
241
|
+
var TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1e3;
|
|
242
|
+
var TOKEN_REQUEST_TIMEOUT_MS = 30 * 1e3;
|
|
243
|
+
var REFRESH_LOCK_FILE = "codex-refresh.lock";
|
|
244
|
+
var REFRESH_LOCK_STALE_MS = 60 * 1e3;
|
|
245
|
+
var REFRESH_LOCK_WAIT_MS = 90 * 1e3;
|
|
246
|
+
var REFRESH_LOCK_POLL_MS = 100;
|
|
243
247
|
|
|
244
248
|
// ../../packages/providers-codex/dist/pkce.js
|
|
245
249
|
import { createHash, randomBytes } from "node:crypto";
|
|
@@ -282,7 +286,7 @@ function callbackPage(title, message) {
|
|
|
282
286
|
return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head><body style="font-family:sans-serif;padding:2rem"><h1>${title}</h1><p>${message}</p></body></html>`;
|
|
283
287
|
}
|
|
284
288
|
async function waitForOAuthCallback(timeoutMs = FLOW_TIMEOUT_MINUTES * 60 * 1e3, port = LOOPBACK_PORT) {
|
|
285
|
-
return new Promise((
|
|
289
|
+
return new Promise((resolve4, reject) => {
|
|
286
290
|
let server;
|
|
287
291
|
const timer = setTimeout(() => {
|
|
288
292
|
closeServer();
|
|
@@ -322,7 +326,7 @@ async function waitForOAuthCallback(timeoutMs = FLOW_TIMEOUT_MINUTES * 60 * 1e3,
|
|
|
322
326
|
res.writeHead(200, { "content-type": "text/html" });
|
|
323
327
|
res.end(callbackPage("Connected", "ImageGenCli is connected. You can close this tab."));
|
|
324
328
|
closeServer();
|
|
325
|
-
|
|
329
|
+
resolve4({ code, state });
|
|
326
330
|
} catch (error2) {
|
|
327
331
|
fail2(error2 instanceof Error ? error2 : new LoopbackError("OAuth callback failed.", "callback_error"));
|
|
328
332
|
}
|
|
@@ -340,7 +344,7 @@ async function waitForOAuthCallback(timeoutMs = FLOW_TIMEOUT_MINUTES * 60 * 1e3,
|
|
|
340
344
|
|
|
341
345
|
// ../../packages/providers-codex/dist/codex-auth.js
|
|
342
346
|
var ReauthRequiredError = class extends Error {
|
|
343
|
-
constructor(message = "Codex credentials are missing or expired. Run `imagegen auth login` to reconnect.") {
|
|
347
|
+
constructor(message = "Codex credentials are missing or expired. Run `imagegen auth codex login` to reconnect.") {
|
|
344
348
|
super(message);
|
|
345
349
|
this.name = "ReauthRequiredError";
|
|
346
350
|
}
|
|
@@ -353,10 +357,24 @@ function toTokenSet(body, fallbackRefreshToken) {
|
|
|
353
357
|
scope: body.scope
|
|
354
358
|
};
|
|
355
359
|
}
|
|
360
|
+
var REJECTED_GRANT_CODES = /* @__PURE__ */ new Set([
|
|
361
|
+
"invalid_grant",
|
|
362
|
+
"invalid_token",
|
|
363
|
+
"refresh_token_expired",
|
|
364
|
+
"refresh_token_reused",
|
|
365
|
+
"refresh_token_invalidated"
|
|
366
|
+
]);
|
|
356
367
|
async function readOAuthErrorCode(response) {
|
|
357
368
|
try {
|
|
358
369
|
const body = await response.json();
|
|
359
|
-
|
|
370
|
+
if (typeof body.error === "string")
|
|
371
|
+
return body.error;
|
|
372
|
+
if (typeof body.error === "object" && body.error !== null) {
|
|
373
|
+
const code = body.error["code"];
|
|
374
|
+
if (typeof code === "string")
|
|
375
|
+
return code;
|
|
376
|
+
}
|
|
377
|
+
return null;
|
|
360
378
|
} catch {
|
|
361
379
|
return null;
|
|
362
380
|
}
|
|
@@ -365,30 +383,31 @@ async function postTokenRequest(params) {
|
|
|
365
383
|
const response = await fetch(TOKEN_ENDPOINT, {
|
|
366
384
|
method: "POST",
|
|
367
385
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
368
|
-
body: new URLSearchParams(params)
|
|
386
|
+
body: new URLSearchParams(params),
|
|
387
|
+
signal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS)
|
|
369
388
|
});
|
|
370
389
|
if (!response.ok) {
|
|
371
390
|
const errorCode = await readOAuthErrorCode(response.clone());
|
|
372
|
-
if (response.status === 401 ||
|
|
373
|
-
return
|
|
391
|
+
if (response.status === 401 || errorCode !== null && REJECTED_GRANT_CODES.has(errorCode)) {
|
|
392
|
+
return { ok: false, reason: errorCode ?? `status ${response.status}` };
|
|
374
393
|
}
|
|
375
|
-
throw new Error(`Token request failed with status ${response.status}.`);
|
|
394
|
+
throw new Error(`Token request failed with status ${response.status}${errorCode ? ` (${errorCode})` : ""}.`);
|
|
376
395
|
}
|
|
377
396
|
const body = await response.json();
|
|
378
|
-
return toTokenSet(body, params["refresh_token"]);
|
|
397
|
+
return { ok: true, tokens: toTokenSet(body, params["refresh_token"]) };
|
|
379
398
|
}
|
|
380
399
|
async function exchangeCodeForTokens(args) {
|
|
381
|
-
const
|
|
400
|
+
const outcome = await postTokenRequest({
|
|
382
401
|
grant_type: "authorization_code",
|
|
383
402
|
client_id: CLIENT_ID,
|
|
384
403
|
code: args.code,
|
|
385
404
|
redirect_uri: args.redirectUri,
|
|
386
405
|
code_verifier: args.codeVerifier
|
|
387
406
|
});
|
|
388
|
-
if (!
|
|
389
|
-
throw new ReauthRequiredError(
|
|
407
|
+
if (!outcome.ok) {
|
|
408
|
+
throw new ReauthRequiredError(`The authorization code was rejected (${outcome.reason}). Run \`imagegen auth codex login\` again.`);
|
|
390
409
|
}
|
|
391
|
-
return tokens;
|
|
410
|
+
return outcome.tokens;
|
|
392
411
|
}
|
|
393
412
|
async function refreshTokens(refreshToken) {
|
|
394
413
|
return postTokenRequest({
|
|
@@ -397,8 +416,8 @@ async function refreshTokens(refreshToken) {
|
|
|
397
416
|
refresh_token: refreshToken
|
|
398
417
|
});
|
|
399
418
|
}
|
|
400
|
-
function
|
|
401
|
-
return tokens.expiresAt <= now;
|
|
419
|
+
function needsRefresh(tokens, now = Date.now()) {
|
|
420
|
+
return tokens.expiresAt - TOKEN_REFRESH_MARGIN_MS <= now;
|
|
402
421
|
}
|
|
403
422
|
var pendingVerifiers = /* @__PURE__ */ new Map();
|
|
404
423
|
function startPkceFlow(redirectUri2) {
|
|
@@ -424,65 +443,9 @@ function prunePendingVerifiers() {
|
|
|
424
443
|
}
|
|
425
444
|
}
|
|
426
445
|
|
|
427
|
-
// ../../packages/providers-codex/dist/
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
pendingRefresh = null;
|
|
431
|
-
constructor(store) {
|
|
432
|
-
this.store = store;
|
|
433
|
-
}
|
|
434
|
-
async exchangeAndSave(args) {
|
|
435
|
-
const tokens = await exchangeCodeForTokens(args);
|
|
436
|
-
await this.store.save(PROVIDER_NAME, tokens);
|
|
437
|
-
return tokens;
|
|
438
|
-
}
|
|
439
|
-
async status() {
|
|
440
|
-
const tokens = await this.store.load(PROVIDER_NAME);
|
|
441
|
-
if (!tokens)
|
|
442
|
-
return { connected: false };
|
|
443
|
-
if (!isTokenExpired(tokens))
|
|
444
|
-
return { connected: true, expiresAt: tokens.expiresAt };
|
|
445
|
-
if (!tokens.refreshToken)
|
|
446
|
-
return { connected: false };
|
|
447
|
-
try {
|
|
448
|
-
const refreshed = await this.getValidAccessToken();
|
|
449
|
-
void refreshed;
|
|
450
|
-
const current = await this.store.load(PROVIDER_NAME);
|
|
451
|
-
return { connected: true, expiresAt: current?.expiresAt };
|
|
452
|
-
} catch {
|
|
453
|
-
return { connected: false };
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
async logout() {
|
|
457
|
-
await this.store.clear(PROVIDER_NAME);
|
|
458
|
-
}
|
|
459
|
-
async getValidAccessToken() {
|
|
460
|
-
const tokens = await this.store.load(PROVIDER_NAME);
|
|
461
|
-
if (!tokens)
|
|
462
|
-
throw new ReauthRequiredError();
|
|
463
|
-
if (!isTokenExpired(tokens))
|
|
464
|
-
return tokens.accessToken;
|
|
465
|
-
if (!tokens.refreshToken) {
|
|
466
|
-
await this.store.clear(PROVIDER_NAME);
|
|
467
|
-
throw new ReauthRequiredError();
|
|
468
|
-
}
|
|
469
|
-
if (!this.pendingRefresh) {
|
|
470
|
-
this.pendingRefresh = this.performRefresh(tokens.refreshToken).finally(() => {
|
|
471
|
-
this.pendingRefresh = null;
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
return this.pendingRefresh;
|
|
475
|
-
}
|
|
476
|
-
async performRefresh(refreshToken) {
|
|
477
|
-
const refreshed = await refreshTokens(refreshToken);
|
|
478
|
-
if (!refreshed) {
|
|
479
|
-
await this.store.clear(PROVIDER_NAME);
|
|
480
|
-
throw new ReauthRequiredError();
|
|
481
|
-
}
|
|
482
|
-
await this.store.save(PROVIDER_NAME, refreshed);
|
|
483
|
-
return refreshed.accessToken;
|
|
484
|
-
}
|
|
485
|
-
};
|
|
446
|
+
// ../../packages/providers-codex/dist/refresh-lock.js
|
|
447
|
+
import { mkdir, open, rm, stat } from "node:fs/promises";
|
|
448
|
+
import { dirname, join as join2 } from "node:path";
|
|
486
449
|
|
|
487
450
|
// ../../packages/credentials/dist/index.js
|
|
488
451
|
import { homedir } from "node:os";
|
|
@@ -490,8 +453,8 @@ import { join } from "node:path";
|
|
|
490
453
|
function asKeytar(module) {
|
|
491
454
|
if (typeof module !== "object" || module === null)
|
|
492
455
|
return null;
|
|
493
|
-
const
|
|
494
|
-
const candidate =
|
|
456
|
+
const record3 = module;
|
|
457
|
+
const candidate = record3["default"] ?? module;
|
|
495
458
|
if (typeof candidate.getPassword === "function" && typeof candidate.setPassword === "function" && typeof candidate.deletePassword === "function") {
|
|
496
459
|
return candidate;
|
|
497
460
|
}
|
|
@@ -582,9 +545,9 @@ var FileCredentialStore = class {
|
|
|
582
545
|
}
|
|
583
546
|
}
|
|
584
547
|
async writeAll(all) {
|
|
585
|
-
const { mkdir:
|
|
586
|
-
const { dirname:
|
|
587
|
-
await
|
|
548
|
+
const { mkdir: mkdir6, writeFile: writeFile4, chmod } = await import("node:fs/promises");
|
|
549
|
+
const { dirname: dirname6 } = await import("node:path");
|
|
550
|
+
await mkdir6(dirname6(this.filePath), { recursive: true });
|
|
588
551
|
await writeFile4(this.filePath, JSON.stringify(all, null, 2), {
|
|
589
552
|
mode: 384
|
|
590
553
|
});
|
|
@@ -602,6 +565,133 @@ function defaultCredentialFilePath() {
|
|
|
602
565
|
return join(base, "ImageGenCli", "tokens.json");
|
|
603
566
|
}
|
|
604
567
|
|
|
568
|
+
// ../../packages/providers-codex/dist/refresh-lock.js
|
|
569
|
+
function defaultRefreshLockPath() {
|
|
570
|
+
return join2(dirname(defaultCredentialFilePath()), REFRESH_LOCK_FILE);
|
|
571
|
+
}
|
|
572
|
+
function createRefreshLock(options = {}) {
|
|
573
|
+
const path = options.path ?? defaultRefreshLockPath();
|
|
574
|
+
const staleMs = options.staleMs ?? REFRESH_LOCK_STALE_MS;
|
|
575
|
+
const waitMs = options.waitMs ?? REFRESH_LOCK_WAIT_MS;
|
|
576
|
+
const pollMs = options.pollMs ?? REFRESH_LOCK_POLL_MS;
|
|
577
|
+
return async (work) => {
|
|
578
|
+
await acquire(path, staleMs, waitMs, pollMs);
|
|
579
|
+
try {
|
|
580
|
+
return await work();
|
|
581
|
+
} finally {
|
|
582
|
+
await rm(path, { force: true });
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
async function acquire(path, staleMs, waitMs, pollMs) {
|
|
587
|
+
await mkdir(dirname(path), { recursive: true });
|
|
588
|
+
const deadline = Date.now() + waitMs;
|
|
589
|
+
for (; ; ) {
|
|
590
|
+
try {
|
|
591
|
+
const handle = await open(path, "wx");
|
|
592
|
+
try {
|
|
593
|
+
await handle.writeFile(`${process.pid}
|
|
594
|
+
`);
|
|
595
|
+
} finally {
|
|
596
|
+
await handle.close();
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
599
|
+
} catch (error2) {
|
|
600
|
+
if (!isContention(error2))
|
|
601
|
+
throw error2;
|
|
602
|
+
}
|
|
603
|
+
if (await isStale(path, staleMs)) {
|
|
604
|
+
await rm(path, { force: true });
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
if (Date.now() >= deadline) {
|
|
608
|
+
throw new Error("Timed out waiting for another imagegen process to refresh Codex credentials.");
|
|
609
|
+
}
|
|
610
|
+
await new Promise((resolve4) => setTimeout(resolve4, pollMs + Math.random() * pollMs));
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function isContention(error2) {
|
|
614
|
+
const code = error2.code;
|
|
615
|
+
return code === "EEXIST" || process.platform === "win32" && (code === "EPERM" || code === "EACCES");
|
|
616
|
+
}
|
|
617
|
+
async function isStale(path, staleMs) {
|
|
618
|
+
try {
|
|
619
|
+
return Date.now() - (await stat(path)).mtimeMs > staleMs;
|
|
620
|
+
} catch {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// ../../packages/providers-codex/dist/codex-token-manager.js
|
|
626
|
+
var CodexTokenManager = class {
|
|
627
|
+
store;
|
|
628
|
+
pendingRefresh = null;
|
|
629
|
+
lock;
|
|
630
|
+
refresh;
|
|
631
|
+
constructor(store, options = {}) {
|
|
632
|
+
this.store = store;
|
|
633
|
+
this.lock = options.lock ?? createRefreshLock();
|
|
634
|
+
this.refresh = options.refresh ?? refreshTokens;
|
|
635
|
+
}
|
|
636
|
+
async exchangeAndSave(args) {
|
|
637
|
+
const tokens = await exchangeCodeForTokens(args);
|
|
638
|
+
await this.store.save(PROVIDER_NAME, tokens);
|
|
639
|
+
return tokens;
|
|
640
|
+
}
|
|
641
|
+
async status() {
|
|
642
|
+
if (!await this.store.load(PROVIDER_NAME))
|
|
643
|
+
return { connected: false };
|
|
644
|
+
try {
|
|
645
|
+
await this.getValidAccessToken();
|
|
646
|
+
const current = await this.store.load(PROVIDER_NAME);
|
|
647
|
+
return { connected: true, expiresAt: current?.expiresAt };
|
|
648
|
+
} catch {
|
|
649
|
+
return { connected: false };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
async logout() {
|
|
653
|
+
await this.store.clear(PROVIDER_NAME);
|
|
654
|
+
}
|
|
655
|
+
async getValidAccessToken() {
|
|
656
|
+
const tokens = await this.store.load(PROVIDER_NAME);
|
|
657
|
+
if (!tokens)
|
|
658
|
+
throw new ReauthRequiredError();
|
|
659
|
+
if (!needsRefresh(tokens))
|
|
660
|
+
return tokens.accessToken;
|
|
661
|
+
return this.refreshFrom(tokens.accessToken);
|
|
662
|
+
}
|
|
663
|
+
// The Codex image endpoint rejected this access token before its recorded expiry.
|
|
664
|
+
async recoverFromUnauthorized(rejectedAccessToken) {
|
|
665
|
+
return this.refreshFrom(rejectedAccessToken);
|
|
666
|
+
}
|
|
667
|
+
refreshFrom(staleAccessToken) {
|
|
668
|
+
if (!this.pendingRefresh) {
|
|
669
|
+
this.pendingRefresh = this.lock(() => this.refreshLocked(staleAccessToken)).finally(() => {
|
|
670
|
+
this.pendingRefresh = null;
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
return this.pendingRefresh;
|
|
674
|
+
}
|
|
675
|
+
async refreshLocked(staleAccessToken) {
|
|
676
|
+
const current = await this.store.load(PROVIDER_NAME);
|
|
677
|
+
if (!current)
|
|
678
|
+
throw new ReauthRequiredError();
|
|
679
|
+
if (current.accessToken !== staleAccessToken && !needsRefresh(current))
|
|
680
|
+
return current.accessToken;
|
|
681
|
+
if (!current.refreshToken) {
|
|
682
|
+
await this.store.clear(PROVIDER_NAME);
|
|
683
|
+
throw new ReauthRequiredError();
|
|
684
|
+
}
|
|
685
|
+
const outcome = await this.refresh(current.refreshToken);
|
|
686
|
+
if (!outcome.ok) {
|
|
687
|
+
await this.store.clear(PROVIDER_NAME);
|
|
688
|
+
throw new ReauthRequiredError(`Codex rejected the stored refresh token (${outcome.reason}). Run \`imagegen auth codex login\` to reconnect.`);
|
|
689
|
+
}
|
|
690
|
+
await this.store.save(PROVIDER_NAME, outcome.tokens);
|
|
691
|
+
return outcome.tokens.accessToken;
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
|
|
605
695
|
// ../../packages/providers-codex/dist/token-store.js
|
|
606
696
|
async function resolveTokenStore() {
|
|
607
697
|
const store = await resolveCredentialStore();
|
|
@@ -642,13 +732,18 @@ function extractAccountId(token) {
|
|
|
642
732
|
return accountId;
|
|
643
733
|
}
|
|
644
734
|
|
|
645
|
-
// ../../packages/providers-codex/dist/
|
|
735
|
+
// ../../packages/providers-codex/dist/codex-client.js
|
|
736
|
+
import sharp from "sharp";
|
|
737
|
+
|
|
738
|
+
// ../../packages/providers-codex/dist/errors.js
|
|
646
739
|
function classifyImageError(message) {
|
|
647
740
|
if (!message.trim())
|
|
648
741
|
return "unknown";
|
|
649
742
|
const lowerRate = message.includes("input-images") && message.includes("per min") && message.toLowerCase().includes("gpt-image");
|
|
650
743
|
if (lowerRate)
|
|
651
744
|
return "codex_image_input_rate_limit";
|
|
745
|
+
if (/usage limit/i.test(message))
|
|
746
|
+
return "usage_limit";
|
|
652
747
|
if (/rate limit|429|too many requests|please try again/i.test(message))
|
|
653
748
|
return "rate_limit";
|
|
654
749
|
if (/403|forbidden|cloudflare/i.test(message))
|
|
@@ -657,107 +752,26 @@ function classifyImageError(message) {
|
|
|
657
752
|
return "image_model_invalid";
|
|
658
753
|
return "api_error";
|
|
659
754
|
}
|
|
660
|
-
function
|
|
661
|
-
if (
|
|
662
|
-
return void 0;
|
|
663
|
-
const candidate = value[property];
|
|
664
|
-
return typeof candidate === "string" ? candidate : void 0;
|
|
665
|
-
}
|
|
666
|
-
function readEventError(event) {
|
|
667
|
-
const direct = readString(event, "message");
|
|
668
|
-
if (direct)
|
|
669
|
-
return direct;
|
|
670
|
-
if (typeof event !== "object" || event === null)
|
|
671
|
-
return void 0;
|
|
672
|
-
const record2 = event;
|
|
673
|
-
const nested = record2["error"];
|
|
674
|
-
if (typeof nested === "object" && nested !== null) {
|
|
675
|
-
const message = readString(nested, "message");
|
|
676
|
-
if (message)
|
|
677
|
-
return message;
|
|
678
|
-
}
|
|
679
|
-
const response = record2["response"];
|
|
680
|
-
if (typeof response === "object" && response !== null) {
|
|
681
|
-
const responseError = response["error"];
|
|
682
|
-
if (typeof responseError === "object" && responseError !== null) {
|
|
683
|
-
return readString(responseError, "message");
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
return void 0;
|
|
687
|
-
}
|
|
688
|
-
function tryReadImageItem(item) {
|
|
689
|
-
if (readString(item, "type") !== "image_generation_call")
|
|
690
|
-
return null;
|
|
691
|
-
const result = readString(item, "result");
|
|
692
|
-
if (!result)
|
|
755
|
+
function readErrorMessage(body) {
|
|
756
|
+
if (!body.trim())
|
|
693
757
|
return null;
|
|
694
|
-
let data;
|
|
695
758
|
try {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
const finish = (image) => ({
|
|
707
|
-
...image,
|
|
708
|
-
responseId,
|
|
709
|
-
eventCount,
|
|
710
|
-
lastEventType
|
|
711
|
-
});
|
|
712
|
-
for (const raw of events) {
|
|
713
|
-
const line = raw.trim();
|
|
714
|
-
if (!line.startsWith("data:"))
|
|
715
|
-
continue;
|
|
716
|
-
const payload = line.slice("data:".length).trim();
|
|
717
|
-
if (payload === "[DONE]")
|
|
718
|
-
break;
|
|
719
|
-
let event;
|
|
720
|
-
try {
|
|
721
|
-
event = JSON.parse(payload);
|
|
722
|
-
} catch {
|
|
723
|
-
continue;
|
|
724
|
-
}
|
|
725
|
-
const type = readString(event, "type");
|
|
726
|
-
lastEventType = type ?? lastEventType;
|
|
727
|
-
eventCount += 1;
|
|
728
|
-
if (type === "response.created" && typeof event === "object" && event !== null) {
|
|
729
|
-
const inner = event["response"];
|
|
730
|
-
const id = readString(inner, "id");
|
|
731
|
-
if (id)
|
|
732
|
-
responseId = id;
|
|
733
|
-
}
|
|
734
|
-
if (type === "response.failed" || type === "error") {
|
|
735
|
-
throw new Error(`${classifyImageError(readEventError(event) ?? "")}: ${readEventError(event) ?? "Codex image generation failed."}`);
|
|
736
|
-
}
|
|
737
|
-
if (type === "response.output_item.done" && typeof event === "object" && event !== null) {
|
|
738
|
-
const item = event["item"];
|
|
739
|
-
const image = tryReadImageItem(item);
|
|
740
|
-
if (image)
|
|
741
|
-
return finish(image);
|
|
742
|
-
}
|
|
743
|
-
if (type === "response.completed" && typeof event === "object" && event !== null) {
|
|
744
|
-
const response = event["response"];
|
|
745
|
-
const id = readString(response, "id");
|
|
746
|
-
if (id)
|
|
747
|
-
responseId = id;
|
|
748
|
-
if (typeof response === "object" && response !== null) {
|
|
749
|
-
const output = response["output"];
|
|
750
|
-
if (Array.isArray(output)) {
|
|
751
|
-
for (const item of output) {
|
|
752
|
-
const image = tryReadImageItem(item);
|
|
753
|
-
if (image)
|
|
754
|
-
return finish(image);
|
|
755
|
-
}
|
|
756
|
-
}
|
|
759
|
+
const parsed = JSON.parse(body);
|
|
760
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
761
|
+
const record3 = parsed;
|
|
762
|
+
if (typeof record3["message"] === "string")
|
|
763
|
+
return record3["message"];
|
|
764
|
+
if (typeof record3["detail"] === "string")
|
|
765
|
+
return record3["detail"];
|
|
766
|
+
const error2 = record3["error"];
|
|
767
|
+
if (typeof error2 === "object" && error2 !== null && typeof error2["message"] === "string") {
|
|
768
|
+
return error2["message"];
|
|
757
769
|
}
|
|
758
770
|
}
|
|
771
|
+
} catch {
|
|
772
|
+
return body.slice(0, 500);
|
|
759
773
|
}
|
|
760
|
-
|
|
774
|
+
return null;
|
|
761
775
|
}
|
|
762
776
|
|
|
763
777
|
// ../../packages/providers-codex/dist/codex-client.js
|
|
@@ -779,112 +793,39 @@ function resolveSize(size) {
|
|
|
779
793
|
});
|
|
780
794
|
}
|
|
781
795
|
}
|
|
782
|
-
function
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
model: args.imageModel,
|
|
786
|
-
action: args.action,
|
|
787
|
-
size: args.size && args.size.trim() ? args.size.trim() : "auto",
|
|
788
|
-
output_format: args.outputFormat,
|
|
789
|
-
background: "auto"
|
|
790
|
-
};
|
|
791
|
-
if (args.partialImages > 0)
|
|
792
|
-
tool["partial_images"] = Math.min(3, Math.max(0, args.partialImages));
|
|
793
|
-
if (args.quality && args.quality.trim() && args.quality.trim().toLowerCase() !== "auto") {
|
|
794
|
-
tool["quality"] = args.quality.trim();
|
|
795
|
-
}
|
|
796
|
-
if (args.outputFormat === "jpeg" && typeof args.outputCompression === "number") {
|
|
797
|
-
tool["output_compression"] = Math.min(100, Math.max(0, Math.trunc(args.outputCompression)));
|
|
798
|
-
}
|
|
799
|
-
return tool;
|
|
800
|
-
}
|
|
801
|
-
function buildPayload(args) {
|
|
802
|
-
const content = [{ type: "input_text", text: args.prompt }];
|
|
803
|
-
for (const reference of args.references.slice(0, MAX_REFERENCE_IMAGES2)) {
|
|
804
|
-
content.push({ type: "input_image", image_url: toDataUrl(reference), detail: "auto" });
|
|
805
|
-
}
|
|
806
|
-
return {
|
|
796
|
+
function buildBody(args) {
|
|
797
|
+
const body = {
|
|
798
|
+
prompt: args.prompt,
|
|
807
799
|
model: args.model,
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
tool_choice: { type: "image_generation" },
|
|
811
|
-
stream: true,
|
|
812
|
-
store: false
|
|
800
|
+
size: args.size.size,
|
|
801
|
+
background: "auto"
|
|
813
802
|
};
|
|
803
|
+
const quality = args.quality.trim();
|
|
804
|
+
if (quality && quality.toLowerCase() !== "auto")
|
|
805
|
+
body["quality"] = quality;
|
|
806
|
+
if (args.images.length > 0)
|
|
807
|
+
body["images"] = args.images.map((image) => ({ image_url: toDataUrl(image) }));
|
|
808
|
+
return body;
|
|
814
809
|
}
|
|
815
810
|
var CodexImageProvider = class {
|
|
816
811
|
tokens;
|
|
817
812
|
name = PROVIDER_NAME;
|
|
818
813
|
sizePolicy = { resolveSize: (size) => resolveSize(size) };
|
|
819
|
-
orchestrationModel;
|
|
820
814
|
imageModel;
|
|
821
815
|
timeoutSeconds;
|
|
822
|
-
partialImages;
|
|
823
816
|
fetchImpl;
|
|
824
817
|
constructor(tokens, options = {}) {
|
|
825
818
|
this.tokens = tokens;
|
|
826
|
-
this.orchestrationModel = options.model ?? process.env["IMAGEGEN_CODEX_MODEL"] ?? DEFAULT_ORCHESTRATION_MODEL;
|
|
827
819
|
this.imageModel = options.imageModel ?? process.env["IMAGEGEN_CODEX_IMAGE_MODEL"] ?? DEFAULT_IMAGE_MODEL;
|
|
828
820
|
this.timeoutSeconds = options.timeoutSeconds ?? REQUEST_TIMEOUT_SECONDS;
|
|
829
|
-
this.partialImages = options.partialImages ?? PARTIAL_IMAGES;
|
|
830
821
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
831
822
|
}
|
|
832
823
|
async generate(request) {
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
const imageModel2 = this.resolveImageModel(request);
|
|
836
|
-
return this.runAll({
|
|
837
|
-
format,
|
|
838
|
-
imageModel: imageModel2,
|
|
839
|
-
count: normalizeCount(request.count),
|
|
840
|
-
prompt: request.prompt,
|
|
841
|
-
size,
|
|
842
|
-
build: () => buildPayload({
|
|
843
|
-
model: this.orchestrationModel,
|
|
844
|
-
prompt: request.prompt,
|
|
845
|
-
references: request.referenceImages ?? [],
|
|
846
|
-
tool: baseImageTool({
|
|
847
|
-
size: size.size,
|
|
848
|
-
quality: request.quality ?? DEFAULT_QUALITY,
|
|
849
|
-
outputFormat: format,
|
|
850
|
-
imageModel: imageModel2,
|
|
851
|
-
action: "generate",
|
|
852
|
-
partialImages: this.partialImages
|
|
853
|
-
})
|
|
854
|
-
})
|
|
855
|
-
});
|
|
824
|
+
const references = (request.referenceImages ?? []).slice(0, MAX_REFERENCE_IMAGES2);
|
|
825
|
+
return this.runAll(request, references.length > 0 ? "edits" : "generations", references);
|
|
856
826
|
}
|
|
857
827
|
async edit(request) {
|
|
858
|
-
|
|
859
|
-
const size = this.sizePolicy.resolveSize(request.size);
|
|
860
|
-
const imageModel2 = this.resolveImageModel(request);
|
|
861
|
-
const references = [request.sourceImage, ...request.referenceImages ?? []];
|
|
862
|
-
return this.runAll({
|
|
863
|
-
format,
|
|
864
|
-
imageModel: imageModel2,
|
|
865
|
-
count: normalizeCount(request.count),
|
|
866
|
-
prompt: request.prompt,
|
|
867
|
-
size,
|
|
868
|
-
build: () => {
|
|
869
|
-
const tool = baseImageTool({
|
|
870
|
-
size: size.size,
|
|
871
|
-
quality: request.quality ?? DEFAULT_QUALITY,
|
|
872
|
-
outputFormat: format,
|
|
873
|
-
imageModel: imageModel2,
|
|
874
|
-
action: "edit",
|
|
875
|
-
partialImages: this.partialImages
|
|
876
|
-
});
|
|
877
|
-
if (request.mask) {
|
|
878
|
-
tool["input_image_mask"] = { image_url: toDataUrl(request.mask) };
|
|
879
|
-
}
|
|
880
|
-
return buildPayload({
|
|
881
|
-
model: this.orchestrationModel,
|
|
882
|
-
prompt: request.prompt,
|
|
883
|
-
references,
|
|
884
|
-
tool
|
|
885
|
-
});
|
|
886
|
-
}
|
|
887
|
-
});
|
|
828
|
+
return this.runAll(request, "edits", [request.sourceImage, ...request.referenceImages ?? []].slice(0, MAX_REFERENCE_IMAGES2));
|
|
888
829
|
}
|
|
889
830
|
resolveImageModel(request) {
|
|
890
831
|
const model = (request.model ?? this.imageModel).trim();
|
|
@@ -896,116 +837,120 @@ var CodexImageProvider = class {
|
|
|
896
837
|
}
|
|
897
838
|
return model;
|
|
898
839
|
}
|
|
899
|
-
async runAll(
|
|
900
|
-
const
|
|
901
|
-
|
|
902
|
-
|
|
840
|
+
async runAll(request, endpoint, images) {
|
|
841
|
+
const format = normalizeOutputFormat(request.format);
|
|
842
|
+
const size = this.sizePolicy.resolveSize(request.size);
|
|
843
|
+
const imageModel2 = this.resolveImageModel(request);
|
|
844
|
+
const body = JSON.stringify(buildBody({ prompt: request.prompt, model: imageModel2, size, quality: request.quality ?? DEFAULT_QUALITY, images }));
|
|
845
|
+
const results = [];
|
|
846
|
+
for (let i = 0, count = normalizeCount(request.count); i < count; i += 1) {
|
|
847
|
+
results.push(await this.sendOnce(endpoint, body, format, request.prompt));
|
|
848
|
+
}
|
|
849
|
+
return { provider: this.name, imageModel: imageModel2, images: results };
|
|
850
|
+
}
|
|
851
|
+
async sendOnce(endpoint, body, format, prompt) {
|
|
852
|
+
let accessToken = await this.tokens.getValidAccessToken();
|
|
853
|
+
let response = await this.post(endpoint, accessToken, body);
|
|
854
|
+
if (response.status === 401) {
|
|
855
|
+
await response.body?.cancel();
|
|
856
|
+
accessToken = await this.tokens.recoverFromUnauthorized(accessToken);
|
|
857
|
+
response = await this.post(endpoint, accessToken, body);
|
|
858
|
+
}
|
|
859
|
+
const requestId = response.headers.get("x-codex-imagegen-request-id") ?? response.headers.get("x-oai-request-id") ?? response.headers.get("x-request-id") ?? void 0;
|
|
860
|
+
if (!response.ok) {
|
|
861
|
+
const errorText = await response.text().catch(() => "");
|
|
862
|
+
const kind = classifyImageError(errorText);
|
|
863
|
+
const detail = readErrorMessage(errorText);
|
|
864
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
865
|
+
throw new ProviderError({
|
|
866
|
+
kind,
|
|
867
|
+
message: detail ? `Codex image request returned ${response.status} (${kind}): ${detail}` : `Codex image request returned ${response.status} (${kind}).`,
|
|
868
|
+
statusCode: response.status,
|
|
869
|
+
requestId,
|
|
870
|
+
...Number.isFinite(retryAfter) && retryAfter > 0 ? { retryAfterSeconds: retryAfter } : {}
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
const payload = record(await response.json().catch(() => null));
|
|
874
|
+
const item = Array.isArray(payload["data"]) ? record(payload["data"][0]) : {};
|
|
875
|
+
if (typeof item["b64_json"] !== "string" || !item["b64_json"]) {
|
|
876
|
+
throw new ProviderError({ kind: "no_image", message: "Codex image request returned no image data.", requestId });
|
|
877
|
+
}
|
|
878
|
+
let data = Buffer.from(item["b64_json"], "base64");
|
|
879
|
+
let width;
|
|
880
|
+
let height;
|
|
881
|
+
try {
|
|
882
|
+
const metadata = await sharp(data).metadata();
|
|
883
|
+
if (!metadata.width || !metadata.height || !["png", "jpeg", "webp"].includes(metadata.format ?? ""))
|
|
884
|
+
throw new Error();
|
|
885
|
+
await sharp(data).stats();
|
|
886
|
+
if (metadata.format !== format)
|
|
887
|
+
data = await sharp(data).toFormat(format).toBuffer();
|
|
888
|
+
width = metadata.width;
|
|
889
|
+
height = metadata.height;
|
|
890
|
+
} catch {
|
|
891
|
+
throw new ProviderError({ kind: "malformed", message: "Codex returned an invalid raster image.", requestId });
|
|
903
892
|
}
|
|
904
|
-
return {
|
|
893
|
+
return {
|
|
894
|
+
data,
|
|
895
|
+
mimeType: `image/${format}`,
|
|
896
|
+
width,
|
|
897
|
+
height,
|
|
898
|
+
prompt,
|
|
899
|
+
responseId: typeof item["generation_id"] === "string" ? item["generation_id"] : void 0,
|
|
900
|
+
usage: {
|
|
901
|
+
requestId,
|
|
902
|
+
imageGeneration: payload["usage"],
|
|
903
|
+
// What the backend actually rendered; it may override requested size and quality.
|
|
904
|
+
served: { size: payload["size"], quality: payload["quality"], background: payload["background"] },
|
|
905
|
+
planLimits: readPlanLimits(response.headers)
|
|
906
|
+
}
|
|
907
|
+
};
|
|
905
908
|
}
|
|
906
|
-
async
|
|
907
|
-
const accessToken = await this.tokens.getValidAccessToken();
|
|
908
|
-
const accountId = extractAccountId(accessToken);
|
|
909
|
+
async post(endpoint, accessToken, body) {
|
|
909
910
|
const controller = new AbortController();
|
|
910
911
|
const timer = setTimeout(() => controller.abort(), this.timeoutSeconds * 1e3);
|
|
911
|
-
let response;
|
|
912
912
|
try {
|
|
913
|
-
|
|
913
|
+
return await this.fetchImpl(`${IMAGES_ENDPOINT}/${endpoint}`, {
|
|
914
914
|
method: "POST",
|
|
915
915
|
headers: {
|
|
916
916
|
authorization: `Bearer ${accessToken}`,
|
|
917
|
-
"chatgpt-account-id":
|
|
918
|
-
"OpenAI-Beta": "responses=experimental",
|
|
917
|
+
"chatgpt-account-id": extractAccountId(accessToken),
|
|
919
918
|
originator: "pi",
|
|
920
919
|
"user-agent": USER_AGENT,
|
|
921
|
-
accept: "
|
|
920
|
+
accept: "application/json",
|
|
922
921
|
"content-type": "application/json"
|
|
923
922
|
},
|
|
924
|
-
body
|
|
923
|
+
body,
|
|
925
924
|
signal: controller.signal
|
|
926
925
|
});
|
|
927
926
|
} finally {
|
|
928
927
|
clearTimeout(timer);
|
|
929
928
|
}
|
|
930
|
-
const requestId = response.headers.get("x-request-id") ?? response.headers.get("openai-request-id") ?? response.headers.get("request-id") ?? void 0;
|
|
931
|
-
if (!response.ok || !response.body) {
|
|
932
|
-
const body = await response.text().catch(() => "");
|
|
933
|
-
const kind = classifyImageError(body);
|
|
934
|
-
const detail = readErrorMessage(body);
|
|
935
|
-
throw new ProviderError({
|
|
936
|
-
kind,
|
|
937
|
-
message: detail ? `Codex image request returned ${response.status} (${kind}): ${detail}` : `Codex image request returned ${response.status} (${kind}).`,
|
|
938
|
-
statusCode: response.status,
|
|
939
|
-
requestId
|
|
940
|
-
});
|
|
941
|
-
}
|
|
942
|
-
const lines = await readSseLines(response.body);
|
|
943
|
-
let parsed;
|
|
944
|
-
try {
|
|
945
|
-
parsed = parseImageStreamEvents(lines);
|
|
946
|
-
} catch (error2) {
|
|
947
|
-
throw toProviderError(error2, requestId);
|
|
948
|
-
}
|
|
949
|
-
return {
|
|
950
|
-
data: parsed.data,
|
|
951
|
-
mimeType: `image/${format}`,
|
|
952
|
-
width: size.width,
|
|
953
|
-
height: size.height,
|
|
954
|
-
prompt,
|
|
955
|
-
responseId: parsed.responseId,
|
|
956
|
-
callId: parsed.callId
|
|
957
|
-
};
|
|
958
929
|
}
|
|
959
930
|
};
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
function readErrorMessage(body) {
|
|
984
|
-
if (!body.trim())
|
|
985
|
-
return null;
|
|
986
|
-
try {
|
|
987
|
-
const parsed = JSON.parse(body);
|
|
988
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
989
|
-
const record2 = parsed;
|
|
990
|
-
if (typeof record2["message"] === "string")
|
|
991
|
-
return record2["message"];
|
|
992
|
-
if (typeof record2["detail"] === "string")
|
|
993
|
-
return record2["detail"];
|
|
994
|
-
const error2 = record2["error"];
|
|
995
|
-
if (typeof error2 === "object" && error2 !== null && typeof error2["message"] === "string") {
|
|
996
|
-
return error2["message"];
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
} catch {
|
|
1000
|
-
return body.slice(0, 500);
|
|
1001
|
-
}
|
|
1002
|
-
return null;
|
|
1003
|
-
}
|
|
1004
|
-
function toProviderError(error2, requestId) {
|
|
1005
|
-
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
1006
|
-
const separator = message.indexOf(":");
|
|
1007
|
-
const kind = separator > 0 ? message.slice(0, separator) : "api_error";
|
|
1008
|
-
return new ProviderError({ kind, message, requestId });
|
|
931
|
+
function record(value) {
|
|
932
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
933
|
+
}
|
|
934
|
+
function readPlanLimits(headers) {
|
|
935
|
+
const number = (name) => {
|
|
936
|
+
const value = headers.get(`x-codex-${name}`);
|
|
937
|
+
const parsed = value === null ? Number.NaN : Number(value);
|
|
938
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
939
|
+
};
|
|
940
|
+
const window = (name) => {
|
|
941
|
+
const usedPercent = number(`${name}-used-percent`);
|
|
942
|
+
const windowMinutes = number(`${name}-window-minutes`);
|
|
943
|
+
if (usedPercent === void 0 || !windowMinutes)
|
|
944
|
+
return void 0;
|
|
945
|
+
return { usedPercent, windowMinutes, resetAt: number(`${name}-reset-at`) };
|
|
946
|
+
};
|
|
947
|
+
const limits = {
|
|
948
|
+
planType: headers.get("x-codex-plan-type") ?? void 0,
|
|
949
|
+
activeLimit: headers.get("x-codex-active-limit") ?? void 0,
|
|
950
|
+
primary: window("primary"),
|
|
951
|
+
secondary: window("secondary")
|
|
952
|
+
};
|
|
953
|
+
return Object.values(limits).some((value) => value !== void 0) ? limits : void 0;
|
|
1009
954
|
}
|
|
1010
955
|
|
|
1011
956
|
// ../../packages/providers-codex/dist/codex-entry.js
|
|
@@ -1049,7 +994,7 @@ function createCodexEntry() {
|
|
|
1049
994
|
}
|
|
1050
995
|
|
|
1051
996
|
// ../../packages/providers-google/dist/gemini-client.js
|
|
1052
|
-
import
|
|
997
|
+
import sharp2 from "sharp";
|
|
1053
998
|
|
|
1054
999
|
// ../../packages/providers-google/dist/image-size.js
|
|
1055
1000
|
var DEFAULT_IMAGE_MODEL2 = "gemini-nano-banana-2.1";
|
|
@@ -1244,12 +1189,12 @@ var GeminiImageProvider = class {
|
|
|
1244
1189
|
if (typeof image.data !== "string" || !image.data || !/^[A-Za-z0-9+/]+={0,2}$/.test(image.data) || image.data.length % 4 !== 0)
|
|
1245
1190
|
throw error("malformed", "Gemini returned invalid inline image data.");
|
|
1246
1191
|
const original = Buffer.from(image.data, "base64");
|
|
1247
|
-
const metadata = await
|
|
1192
|
+
const metadata = await sharp2(original).metadata().catch(() => {
|
|
1248
1193
|
throw error("malformed", "Gemini returned undecodable image bytes.");
|
|
1249
1194
|
});
|
|
1250
1195
|
if (!metadata.width || !metadata.height || !["png", "jpeg", "webp"].includes(metadata.format ?? "") || image.mime_type !== `image/${metadata.format}`)
|
|
1251
1196
|
throw error("malformed", "Gemini returned an invalid image or inconsistent MIME type.");
|
|
1252
|
-
const data = metadata.format === format ? original : await
|
|
1197
|
+
const data = metadata.format === format ? original : await sharp2(original).toFormat(format).toBuffer();
|
|
1253
1198
|
return {
|
|
1254
1199
|
data,
|
|
1255
1200
|
mimeType: `image/${format}`,
|
|
@@ -1315,8 +1260,8 @@ function createGeminiEntry() {
|
|
|
1315
1260
|
}
|
|
1316
1261
|
|
|
1317
1262
|
// ../../packages/providers-configurable/dist/profiles.js
|
|
1318
|
-
import { readFile, mkdir, writeFile, rename, rm } from "node:fs/promises";
|
|
1319
|
-
import { dirname, join as
|
|
1263
|
+
import { readFile, mkdir as mkdir2, writeFile, rename, rm as rm2 } from "node:fs/promises";
|
|
1264
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
1320
1265
|
import { randomUUID } from "node:crypto";
|
|
1321
1266
|
var usage = (message) => new ProviderError({ kind: "usage", message });
|
|
1322
1267
|
var profileStore = () => resolveCredentialStore();
|
|
@@ -1366,7 +1311,7 @@ function validateProfile(value) {
|
|
|
1366
1311
|
var ProfileRepository = class {
|
|
1367
1312
|
path;
|
|
1368
1313
|
credentials;
|
|
1369
|
-
constructor(path =
|
|
1314
|
+
constructor(path = join3(dirname2(defaultCredentialFilePath()), "providers.json"), credentials = profileStore) {
|
|
1370
1315
|
this.path = path;
|
|
1371
1316
|
this.credentials = credentials;
|
|
1372
1317
|
}
|
|
@@ -1431,7 +1376,7 @@ var ProfileRepository = class {
|
|
|
1431
1376
|
async save(profiles) {
|
|
1432
1377
|
const temporary = `${this.path}.${randomUUID()}.tmp`;
|
|
1433
1378
|
try {
|
|
1434
|
-
await
|
|
1379
|
+
await mkdir2(dirname2(this.path), { recursive: true });
|
|
1435
1380
|
await writeFile(temporary, JSON.stringify({ version: 1, profiles }, null, 2) + "\n", { mode: 384 });
|
|
1436
1381
|
await rename(temporary, this.path);
|
|
1437
1382
|
} catch {
|
|
@@ -1440,7 +1385,7 @@ var ProfileRepository = class {
|
|
|
1440
1385
|
message: "Could not save endpoint configuration."
|
|
1441
1386
|
});
|
|
1442
1387
|
} finally {
|
|
1443
|
-
await
|
|
1388
|
+
await rm2(temporary, { force: true }).catch(() => {
|
|
1444
1389
|
});
|
|
1445
1390
|
}
|
|
1446
1391
|
}
|
|
@@ -1449,9 +1394,9 @@ async function profileKey(profile, store) {
|
|
|
1449
1394
|
if (profile.auth === "none")
|
|
1450
1395
|
return void 0;
|
|
1451
1396
|
try {
|
|
1452
|
-
const
|
|
1453
|
-
if (
|
|
1454
|
-
return validateApiKey(
|
|
1397
|
+
const record3 = await store.load(profile.name);
|
|
1398
|
+
if (record3 && typeof record3.apiKey === "string")
|
|
1399
|
+
return validateApiKey(record3.apiKey);
|
|
1455
1400
|
} catch {
|
|
1456
1401
|
}
|
|
1457
1402
|
throw new ProviderError({
|
|
@@ -1461,7 +1406,7 @@ async function profileKey(profile, store) {
|
|
|
1461
1406
|
}
|
|
1462
1407
|
|
|
1463
1408
|
// ../../packages/providers-configurable/dist/provider.js
|
|
1464
|
-
import
|
|
1409
|
+
import sharp3 from "sharp";
|
|
1465
1410
|
|
|
1466
1411
|
// ../../packages/providers-configurable/dist/image-size.js
|
|
1467
1412
|
function imageSize(value, api) {
|
|
@@ -1487,7 +1432,7 @@ function imageSize(value, api) {
|
|
|
1487
1432
|
}
|
|
1488
1433
|
|
|
1489
1434
|
// ../../packages/providers-configurable/dist/http.js
|
|
1490
|
-
var
|
|
1435
|
+
var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1491
1436
|
var EndpointHttp = class {
|
|
1492
1437
|
fetchImpl;
|
|
1493
1438
|
timeoutSeconds;
|
|
@@ -1511,9 +1456,9 @@ var EndpointHttp = class {
|
|
|
1511
1456
|
});
|
|
1512
1457
|
if (!response.ok) {
|
|
1513
1458
|
const status = response.status;
|
|
1514
|
-
const failure2 =
|
|
1515
|
-
const detail =
|
|
1516
|
-
const upstream =
|
|
1459
|
+
const failure2 = record2(await response.json().catch(() => null));
|
|
1460
|
+
const detail = record2(failure2.error);
|
|
1461
|
+
const upstream = record2(detail.metadata).provider_name;
|
|
1517
1462
|
const token = new Headers(init.headers).get("authorization")?.replace(/^Bearer\s+/i, "");
|
|
1518
1463
|
const upstreamProvider = typeof upstream === "string" && /^[A-Za-z0-9][A-Za-z0-9 ._()-]{0,63}$/.test(upstream) && !(token && upstream.includes(token)) && !/(?:sk-|AIza)/.test(upstream) ? upstream : void 0;
|
|
1519
1464
|
const retryHeader = response.headers.get("retry-after");
|
|
@@ -1537,7 +1482,7 @@ var EndpointHttp = class {
|
|
|
1537
1482
|
const value = await response.json();
|
|
1538
1483
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1539
1484
|
throw new Error();
|
|
1540
|
-
return
|
|
1485
|
+
return record2(value);
|
|
1541
1486
|
} catch {
|
|
1542
1487
|
throw new ProviderError({
|
|
1543
1488
|
kind: "malformed",
|
|
@@ -1672,14 +1617,14 @@ var ConfigurableImageProvider = class {
|
|
|
1672
1617
|
message: "Model discovery response must contain a data array."
|
|
1673
1618
|
});
|
|
1674
1619
|
return body.data.map((value) => {
|
|
1675
|
-
const model =
|
|
1620
|
+
const model = record2(value);
|
|
1676
1621
|
if (typeof model.id !== "string")
|
|
1677
1622
|
throw new ProviderError({
|
|
1678
1623
|
kind: "malformed",
|
|
1679
1624
|
message: "Discovered model has no string ID."
|
|
1680
1625
|
});
|
|
1681
|
-
const architecture =
|
|
1682
|
-
const supported =
|
|
1626
|
+
const architecture = record2(model.architecture);
|
|
1627
|
+
const supported = record2(model.supported_parameters);
|
|
1683
1628
|
const strings = (v) => Array.isArray(v) && v.every((s) => typeof s === "string") ? v : void 0;
|
|
1684
1629
|
return {
|
|
1685
1630
|
id: model.id,
|
|
@@ -1737,7 +1682,7 @@ var ConfigurableImageProvider = class {
|
|
|
1737
1682
|
kind: "malformed",
|
|
1738
1683
|
message: "Endpoint must return exactly one image for a single-image request."
|
|
1739
1684
|
});
|
|
1740
|
-
const image =
|
|
1685
|
+
const image = record2(body.data[0]);
|
|
1741
1686
|
let data;
|
|
1742
1687
|
if (typeof image.b64_json === "string") {
|
|
1743
1688
|
if (!image.b64_json || image.b64_json.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(image.b64_json))
|
|
@@ -1768,12 +1713,12 @@ var ConfigurableImageProvider = class {
|
|
|
1768
1713
|
message: "Endpoint returned no image bytes or download URL."
|
|
1769
1714
|
});
|
|
1770
1715
|
try {
|
|
1771
|
-
const metadata = await
|
|
1716
|
+
const metadata = await sharp3(data).metadata();
|
|
1772
1717
|
if (!metadata.width || !metadata.height || !["png", "jpeg", "webp"].includes(metadata.format ?? "") || typeof image.media_type === "string" && image.media_type !== `image/${metadata.format}`)
|
|
1773
1718
|
throw new Error();
|
|
1774
|
-
await
|
|
1719
|
+
await sharp3(data).stats();
|
|
1775
1720
|
if (metadata.format !== format)
|
|
1776
|
-
data = await
|
|
1721
|
+
data = await sharp3(data).toFormat(format).toBuffer();
|
|
1777
1722
|
return {
|
|
1778
1723
|
data,
|
|
1779
1724
|
width: metadata.width,
|
|
@@ -1833,7 +1778,7 @@ async function readApiKey(fromStdin, input = process.stdin, output = process.std
|
|
|
1833
1778
|
const wasRaw = terminal.isRaw;
|
|
1834
1779
|
const wasFlowing = input.readableFlowing === true;
|
|
1835
1780
|
emitKeypressEvents(input);
|
|
1836
|
-
return new Promise((
|
|
1781
|
+
return new Promise((resolve4, reject) => {
|
|
1837
1782
|
let text = "";
|
|
1838
1783
|
const cleanup = () => {
|
|
1839
1784
|
input.removeListener("keypress", onKey);
|
|
@@ -1858,7 +1803,7 @@ async function readApiKey(fromStdin, input = process.stdin, output = process.std
|
|
|
1858
1803
|
}
|
|
1859
1804
|
if (key.name === "return" || key.name === "enter") {
|
|
1860
1805
|
cleanup();
|
|
1861
|
-
|
|
1806
|
+
resolve4(text);
|
|
1862
1807
|
return;
|
|
1863
1808
|
}
|
|
1864
1809
|
if (key.name === "backspace") {
|
|
@@ -1908,7 +1853,7 @@ Supported protocols: OpenRouter dedicated Images API and OpenAI Images JSON /
|
|
|
1908
1853
|
multipart endpoints, not arbitrary HTTP templates. Options vary by model.
|
|
1909
1854
|
OpenRouter accepts aspect ratios; openai-images needs pixels or presets:
|
|
1910
1855
|
square 1024x1024, portrait 1024x1536, landscape 1536x1024, wide 1536x864.
|
|
1911
|
-
Auto options are omitted. Native masks require openai-images
|
|
1856
|
+
Auto options are omitted. Native masks require openai-images.
|
|
1912
1857
|
Outputs are measured and converted to PNG/JPEG/WebP without resizing. Count uses
|
|
1913
1858
|
separate single-image requests without automatic retries or model substitution.
|
|
1914
1859
|
`;
|
|
@@ -1946,7 +1891,9 @@ Supported Codex models (account availability is determined by the provider):
|
|
|
1946
1891
|
Use full model IDs; other provider-supported IDs may also be supplied.
|
|
1947
1892
|
Model precedence: --model > constructor imageModel (library callers) >
|
|
1948
1893
|
IMAGEGEN_CODEX_IMAGE_MODEL > gpt-image-2.5-flare.
|
|
1949
|
-
|
|
1894
|
+
Codex may override the requested model, quality, and size; results report the
|
|
1895
|
+
returned dimensions. Codex ignores --mask; describe the region in the prompt.
|
|
1896
|
+
IMAGEGEN_USAGE_LOG=<file> appends one prompt-free JSON usage line per result.
|
|
1950
1897
|
|
|
1951
1898
|
Codex quality: auto (default), low, medium, high; Flare/Sunburst also support
|
|
1952
1899
|
xhigh and max. gpt-image-2 rejects xhigh/max.
|
|
@@ -1959,7 +1906,7 @@ var GENERATE_EXAMPLES = `Examples:
|
|
|
1959
1906
|
`;
|
|
1960
1907
|
var EDIT_EXAMPLES = `Edit options:
|
|
1961
1908
|
--source <file> Source image (required)
|
|
1962
|
-
--mask <file> Optional edit mask (Codex
|
|
1909
|
+
--mask <file> Optional edit mask (openai-images; Codex ignores it)
|
|
1963
1910
|
|
|
1964
1911
|
Examples:
|
|
1965
1912
|
imagegen edit --format webp --source ./product.png --prompt "make the product blue" --out ./blue.webp
|
|
@@ -2102,9 +2049,9 @@ ${USAGE}`;
|
|
|
2102
2049
|
}
|
|
2103
2050
|
|
|
2104
2051
|
// src/skill.ts
|
|
2105
|
-
import { mkdir as
|
|
2052
|
+
import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
2106
2053
|
import { homedir as homedir2 } from "node:os";
|
|
2107
|
-
import { dirname as
|
|
2054
|
+
import { dirname as dirname3, join as join4, resolve } from "node:path";
|
|
2108
2055
|
var SKILL_NAME = "imagegen-cli";
|
|
2109
2056
|
var SkillExistsError = class extends Error {
|
|
2110
2057
|
constructor(path) {
|
|
@@ -2116,8 +2063,8 @@ async function skillText() {
|
|
|
2116
2063
|
return readFile2(new URL("../skill/SKILL.md", import.meta.url), "utf8");
|
|
2117
2064
|
}
|
|
2118
2065
|
function targetPath(target) {
|
|
2119
|
-
const root = target === "claude" ?
|
|
2120
|
-
return
|
|
2066
|
+
const root = target === "claude" ? join4(homedir2(), ".claude", "skills") : target === "codex" ? join4(homedir2(), ".agents", "skills") : resolve(target);
|
|
2067
|
+
return join4(root, SKILL_NAME, "SKILL.md");
|
|
2121
2068
|
}
|
|
2122
2069
|
async function existingText(path) {
|
|
2123
2070
|
try {
|
|
@@ -2143,12 +2090,50 @@ async function installSkill(targets, force) {
|
|
|
2143
2090
|
}
|
|
2144
2091
|
for (const item of planned.values()) {
|
|
2145
2092
|
if (item.status === "up-to-date") continue;
|
|
2146
|
-
await
|
|
2093
|
+
await mkdir3(dirname3(item.path), { recursive: true });
|
|
2147
2094
|
await writeFile2(item.path, content, { encoding: "utf8", flag: item.status === "installed" ? "wx" : "w" });
|
|
2148
2095
|
}
|
|
2149
2096
|
return [...planned.values()];
|
|
2150
2097
|
}
|
|
2151
2098
|
|
|
2099
|
+
// src/usage-log.ts
|
|
2100
|
+
import { appendFile, mkdir as mkdir4 } from "node:fs/promises";
|
|
2101
|
+
import { dirname as dirname4, resolve as resolve2 } from "node:path";
|
|
2102
|
+
var USAGE_LOG_ENV = "IMAGEGEN_USAGE_LOG";
|
|
2103
|
+
function usageLogEntry(request, edit, result, paths) {
|
|
2104
|
+
return {
|
|
2105
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2106
|
+
command: edit ? "edit" : "generate",
|
|
2107
|
+
provider: result.provider,
|
|
2108
|
+
imageModel: result.imageModel,
|
|
2109
|
+
requested: {
|
|
2110
|
+
size: request.size ?? "auto",
|
|
2111
|
+
quality: request.quality ?? "auto",
|
|
2112
|
+
format: request.format ?? "png",
|
|
2113
|
+
count: request.count ?? 1,
|
|
2114
|
+
references: request.referenceImages?.length ?? 0,
|
|
2115
|
+
mask: edit ? edit.mask !== void 0 : void 0
|
|
2116
|
+
},
|
|
2117
|
+
images: result.images.map((image, i) => ({
|
|
2118
|
+
path: paths[i],
|
|
2119
|
+
width: image.width,
|
|
2120
|
+
height: image.height,
|
|
2121
|
+
bytes: image.data.byteLength,
|
|
2122
|
+
responseId: image.responseId,
|
|
2123
|
+
usage: image.usage
|
|
2124
|
+
}))
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
async function appendUsageLog(entry) {
|
|
2128
|
+
const target = process.env[USAGE_LOG_ENV]?.trim();
|
|
2129
|
+
if (!target) return void 0;
|
|
2130
|
+
const path = resolve2(target);
|
|
2131
|
+
await mkdir4(dirname4(path), { recursive: true });
|
|
2132
|
+
await appendFile(path, `${JSON.stringify(entry)}
|
|
2133
|
+
`, "utf8");
|
|
2134
|
+
return path;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2152
2137
|
// src/index.ts
|
|
2153
2138
|
var EXIT_USAGE = 2;
|
|
2154
2139
|
var EXIT_AUTH = 3;
|
|
@@ -2277,10 +2262,10 @@ function requestedProviders(flags) {
|
|
|
2277
2262
|
return raw;
|
|
2278
2263
|
}
|
|
2279
2264
|
async function resolveOutPaths(out, providerName, multiProvider, count, format) {
|
|
2280
|
-
const resolved =
|
|
2265
|
+
const resolved = resolve3(out);
|
|
2281
2266
|
let isDir = false;
|
|
2282
2267
|
try {
|
|
2283
|
-
const info = await
|
|
2268
|
+
const info = await stat2(resolved);
|
|
2284
2269
|
isDir = info.isDirectory();
|
|
2285
2270
|
} catch {
|
|
2286
2271
|
isDir = extname(resolved) === "";
|
|
@@ -2288,12 +2273,12 @@ async function resolveOutPaths(out, providerName, multiProvider, count, format)
|
|
|
2288
2273
|
const extension = `.${normalizeOutputFormat(format)}`;
|
|
2289
2274
|
const prefix = multiProvider ? `${providerName}-` : "";
|
|
2290
2275
|
if (isDir) {
|
|
2291
|
-
return Array.from({ length: count }, (_, i) =>
|
|
2276
|
+
return Array.from({ length: count }, (_, i) => join5(resolved, `${prefix}image-${i + 1}${extension}`));
|
|
2292
2277
|
}
|
|
2293
2278
|
if (count > 1 || multiProvider) {
|
|
2294
|
-
const dir =
|
|
2279
|
+
const dir = dirname5(resolved);
|
|
2295
2280
|
const base = basename(resolved, extname(resolved));
|
|
2296
|
-
return Array.from({ length: count }, (_, i) =>
|
|
2281
|
+
return Array.from({ length: count }, (_, i) => join5(dir, `${prefix}${base}-${i + 1}${extension}`));
|
|
2297
2282
|
}
|
|
2298
2283
|
return [resolved.endsWith(extension) ? resolved : `${resolved}${extension}`];
|
|
2299
2284
|
}
|
|
@@ -2318,7 +2303,7 @@ async function saveImages(result, paths) {
|
|
|
2318
2303
|
for (let i = 0; i < result.images.length; i += 1) {
|
|
2319
2304
|
const path = paths[i];
|
|
2320
2305
|
try {
|
|
2321
|
-
await
|
|
2306
|
+
await mkdir5(dirname5(path), { recursive: true });
|
|
2322
2307
|
await writeFile3(path, Buffer.from(result.images[i].data));
|
|
2323
2308
|
} catch {
|
|
2324
2309
|
fail("io", `Could not write output file: ${path}`, EXIT_IO);
|
|
@@ -2391,8 +2376,8 @@ async function runAuthLogin(providerName, flags) {
|
|
|
2391
2376
|
const waiter = waitForOAuthCallback(timeoutMinutes * 60 * 1e3, port);
|
|
2392
2377
|
if (!flags.get("no-open")) {
|
|
2393
2378
|
try {
|
|
2394
|
-
const { default:
|
|
2395
|
-
await
|
|
2379
|
+
const { default: open2 } = await import("open");
|
|
2380
|
+
await open2(url);
|
|
2396
2381
|
} catch {
|
|
2397
2382
|
process.stderr.write("Could not open a browser automatically. Open this URL manually:\n");
|
|
2398
2383
|
}
|
|
@@ -2525,6 +2510,12 @@ async function runImageRequest(request, edit, providers) {
|
|
|
2525
2510
|
const paths = await resolveOutPaths(request.out, entry.provider.name, multiProvider, result.images.length, request.format ?? "png");
|
|
2526
2511
|
await saveImages(result, paths);
|
|
2527
2512
|
printResult(entry.provider.name, result, paths);
|
|
2513
|
+
try {
|
|
2514
|
+
await appendUsageLog(usageLogEntry(request, edit, result, paths));
|
|
2515
|
+
} catch {
|
|
2516
|
+
process.stderr.write(`${JSON.stringify({ warning: { kind: "io", message: `Could not append to ${USAGE_LOG_ENV} file.` } })}
|
|
2517
|
+
`);
|
|
2518
|
+
}
|
|
2528
2519
|
}
|
|
2529
2520
|
}
|
|
2530
2521
|
async function main() {
|