egaki 0.3.0 → 0.4.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/CHANGELOG.md +38 -0
- package/README.md +15 -0
- package/dist/chatgpt-auth.d.ts +25 -0
- package/dist/chatgpt-auth.d.ts.map +1 -0
- package/dist/chatgpt-auth.js +18 -11
- package/dist/cli.js +234 -15
- package/dist/credentials.d.ts +17 -4
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +50 -10
- package/dist/login.d.ts +1 -1
- package/dist/login.d.ts.map +1 -1
- package/dist/login.js +44 -3
- package/dist/model-catalog.d.ts.map +1 -1
- package/dist/model-catalog.js +17 -3
- package/dist/models.d.ts +5 -0
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +11 -1
- package/package.json +3 -2
- package/src/chatgpt-auth.ts +387 -0
- package/src/cli.ts +304 -18
- package/src/credentials.ts +70 -17
- package/src/login.ts +49 -4
- package/src/model-catalog.ts +18 -3
- package/src/models.ts +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.4.0
|
|
6
|
+
|
|
7
|
+
1. **ChatGPT login for OpenAI image generation** — use your ChatGPT subscription in `egaki` without a separate OpenAI platform API key:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
egaki login --provider chatgpt
|
|
11
|
+
egaki image "a dreamy studio ghibli style bakery at sunrise" -m gpt-image-1.5 -o bakery.png
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
OpenAI image requests authenticated this way now follow the same Codex backend flow used by ChatGPT instead of the normal Image API path.
|
|
15
|
+
|
|
16
|
+
2. **ChatGPT-backed image editing** — edit existing images with OpenAI image models through the ChatGPT/Codex backend:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
egaki image "change the red jacket to a blue jacket" -m gpt-image-1.5 --input portrait.png -o portrait-blue.png
|
|
20
|
+
egaki image "turn this product shot into a clay render" -m gpt-image-1.5 --input product.png -o product-clay.png
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Multiple input images are supported. For this backend path, `egaki` now explicitly rejects unsupported options like `--seed`, `--mask`, and multi-image output instead of pretending they work.
|
|
24
|
+
|
|
25
|
+
3. **Aspect ratio support for ChatGPT image generation** — supported ChatGPT/OpenAI aspect ratios now map to the backend's real size controls:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
egaki image "wide landscape matte painting" -m gpt-image-1.5 --aspect-ratio 3:2 -o wide.png
|
|
29
|
+
egaki image "book cover concept art" -m gpt-image-1.5 --aspect-ratio 2:3 -o cover.png
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Supported ratios on this path are `1:1`, `3:2`, and `2:3`.
|
|
33
|
+
|
|
34
|
+
4. **`egaki models` now shows login availability** — model discovery output includes whether each provider is currently usable from your configured credentials:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
egaki models
|
|
38
|
+
egaki models --json | jq '.[] | {id, provider, auth}'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The output now includes `auth.available` and `auth.source` (`env`, `stored`, `oauth`, or `none`) so you can see which model families are ready to use.
|
|
42
|
+
|
|
5
43
|
## 0.3.0
|
|
6
44
|
|
|
7
45
|
1. **Google Vertex AI provider** — use Google Cloud billing instead of AI Studio by
|
package/README.md
CHANGED
|
@@ -20,6 +20,14 @@ egaki image "a watercolor fox reading a map" -o fox.png
|
|
|
20
20
|
egaki video "a paper boat drifting on a calm lake at sunrise" -o boat.mp4
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
### Quick start with ChatGPT auth
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
egaki login --provider chatgpt
|
|
27
|
+
egaki image "a dreamy studio ghibli style bakery at sunrise" -m gpt-image-1.5 -o bakery.png
|
|
28
|
+
egaki image "change the red jacket to a blue jacket" -m gpt-image-1.5 --input portrait.png -o portrait-blue.png
|
|
29
|
+
```
|
|
30
|
+
|
|
23
31
|
## CLI examples
|
|
24
32
|
|
|
25
33
|
### Generate an image from text
|
|
@@ -34,6 +42,7 @@ egaki image "isometric floating city, detailed, soft colors" -m imagen-4.0-gener
|
|
|
34
42
|
```bash
|
|
35
43
|
egaki image "add a red scarf and make it winter" --input portrait.jpg -o portrait-winter.png
|
|
36
44
|
egaki image "turn this into a manga panel" --input https://example.com/photo.jpg -o manga.png
|
|
45
|
+
egaki image "change the red square to a blue square" -m gpt-image-1.5 --input input.png -o output.png
|
|
37
46
|
```
|
|
38
47
|
|
|
39
48
|
### Inpainting with a mask
|
|
@@ -53,6 +62,7 @@ egaki image "minimal logo concepts for a cat cafe" -n 4 -o logo.png
|
|
|
53
62
|
```bash
|
|
54
63
|
egaki image "cyberpunk alley at night" --aspect-ratio 16:9
|
|
55
64
|
egaki image "polaroid-style travel photo" --aspect-ratio 4:5
|
|
65
|
+
egaki image "wide landscape matte painting" -m gpt-image-1.5 --aspect-ratio 3:2 -o wide.png
|
|
56
66
|
```
|
|
57
67
|
|
|
58
68
|
### Use Google Cloud billing via Vertex AI
|
|
@@ -209,6 +219,7 @@ egaki image "mascot variations, flat vector look" \
|
|
|
209
219
|
egaki supports **both** authentication modes:
|
|
210
220
|
|
|
211
221
|
- **BYOK (bring your own keys):** add provider keys with `egaki login` per provider.
|
|
222
|
+
- **ChatGPT auth:** log in with `egaki login --provider chatgpt` to use the ChatGPT/Codex backend for supported OpenAI image generation and editing flows.
|
|
212
223
|
- **Egaki subscription:** use one `egaki_...` key to access all supported models without managing keys for each provider.
|
|
213
224
|
- **Google vs Vertex:** bare model IDs (e.g. `imagen-4.0-generate-001`) use Google AI Studio. Prefix with `vertex/` (e.g. `vertex/imagen-4.0-generate-001`) to route through Vertex AI / Google Cloud billing.
|
|
214
225
|
|
|
@@ -226,6 +237,10 @@ egaki login --provider egaki --key egaki_...
|
|
|
226
237
|
egaki login --provider google --key AIza...
|
|
227
238
|
egaki login --provider vertex --key AIza...
|
|
228
239
|
|
|
240
|
+
# ChatGPT OAuth for Codex-backed image generation/editing
|
|
241
|
+
egaki login --provider chatgpt
|
|
242
|
+
egaki image "turn this product shot into a clay render" -m gpt-image-1.5 --input product.png -o product-clay.png
|
|
243
|
+
|
|
229
244
|
# Check subscription usage / cancel
|
|
230
245
|
egaki usage
|
|
231
246
|
egaki unsubscribe
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type ChatGptAuth = {
|
|
2
|
+
email?: string;
|
|
3
|
+
accountId?: string;
|
|
4
|
+
plan?: string;
|
|
5
|
+
refresh: string;
|
|
6
|
+
access: string;
|
|
7
|
+
expires: number;
|
|
8
|
+
};
|
|
9
|
+
/** Extract the ChatGPT plan type (e.g. "plus", "pro") from the access token. */
|
|
10
|
+
export declare function extractPlanType(auth: ChatGptAuth): string | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Run the full ChatGPT OAuth browser flow with clack UI.
|
|
13
|
+
* Returns the ChatGptAuth object to be stored in credentials.json.
|
|
14
|
+
*/
|
|
15
|
+
export declare function chatGptOAuthLogin(): Promise<ChatGptAuth>;
|
|
16
|
+
/**
|
|
17
|
+
* Refresh an expired ChatGPT access token using the refresh token.
|
|
18
|
+
* Returns the updated auth object, or an Error if refresh fails.
|
|
19
|
+
*/
|
|
20
|
+
export declare function refreshChatGptToken(auth: ChatGptAuth): Promise<ChatGptAuth | Error>;
|
|
21
|
+
/**
|
|
22
|
+
* Refresh the auth object if needed and return the full updated state.
|
|
23
|
+
*/
|
|
24
|
+
export declare function getValidChatGptAuth(auth: ChatGptAuth, onRefresh?: (updated: ChatGptAuth) => void): Promise<ChatGptAuth | Error>;
|
|
25
|
+
//# sourceMappingURL=chatgpt-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chatgpt-auth.d.ts","sourceRoot":"","sources":["../src/chatgpt-auth.ts"],"names":[],"mappings":"AAmBA,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAgGD,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAOrE;AA8JD;;;GAGG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,WAAW,CAAC,CA8C9D;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,GAAG,KAAK,CAAC,CAyBzF;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,WAAW,EACjB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,GACzC,OAAO,CAAC,WAAW,GAAG,KAAK,CAAC,CAS9B"}
|
package/dist/chatgpt-auth.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// Tokens are stored inside credentials.json under the 'chatgpt' key as a
|
|
4
4
|
// structured object (not a plain string like other providers).
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
6
|
+
// Tokens are stored so egaki can reuse the ChatGPT login for Codex-style
|
|
7
|
+
// backend requests and refresh them when needed.
|
|
8
8
|
import { createServer } from 'node:http';
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
10
|
import { spinner, log, note } from '@clack/prompts';
|
|
@@ -55,6 +55,13 @@ function extractEmail(tokens) {
|
|
|
55
55
|
return ((typeof idClaims?.email === 'string' ? idClaims.email : undefined) ??
|
|
56
56
|
(typeof accessClaims?.email === 'string' ? accessClaims.email : undefined));
|
|
57
57
|
}
|
|
58
|
+
function extractPlanTypeFromTokens(tokens) {
|
|
59
|
+
const idClaims = tokens.id_token ? parseJwtClaims(tokens.id_token) : undefined;
|
|
60
|
+
const accessClaims = parseJwtClaims(tokens.access_token);
|
|
61
|
+
const idAuth = idClaims?.['https://api.openai.com/auth'];
|
|
62
|
+
const accessAuth = accessClaims?.['https://api.openai.com/auth'];
|
|
63
|
+
return idAuth?.chatgpt_plan_type ?? accessAuth?.chatgpt_plan_type;
|
|
64
|
+
}
|
|
58
65
|
function extractAccountId(tokens) {
|
|
59
66
|
const idClaims = tokens.id_token ? parseJwtClaims(tokens.id_token) : undefined;
|
|
60
67
|
const accessClaims = parseJwtClaims(tokens.access_token);
|
|
@@ -71,6 +78,8 @@ function extractAccountId(tokens) {
|
|
|
71
78
|
}
|
|
72
79
|
/** Extract the ChatGPT plan type (e.g. "plus", "pro") from the access token. */
|
|
73
80
|
export function extractPlanType(auth) {
|
|
81
|
+
if (auth.plan)
|
|
82
|
+
return auth.plan;
|
|
74
83
|
const claims = parseJwtClaims(auth.access);
|
|
75
84
|
const authClaim = claims?.['https://api.openai.com/auth'];
|
|
76
85
|
return authClaim?.chatgpt_plan_type;
|
|
@@ -81,7 +90,7 @@ function buildAuthorizeUrl(pkce, state) {
|
|
|
81
90
|
response_type: 'code',
|
|
82
91
|
client_id: CLIENT_ID,
|
|
83
92
|
redirect_uri: REDIRECT_URI,
|
|
84
|
-
scope: 'openid profile email offline_access',
|
|
93
|
+
scope: 'openid profile email offline_access api.connectors.read api.connectors.invoke',
|
|
85
94
|
code_challenge: pkce.challenge,
|
|
86
95
|
code_challenge_method: 'S256',
|
|
87
96
|
id_token_add_organizations: 'true',
|
|
@@ -236,6 +245,7 @@ export async function chatGptOAuthLogin() {
|
|
|
236
245
|
const auth = {
|
|
237
246
|
email: extractEmail(tokens),
|
|
238
247
|
accountId: extractAccountId(tokens),
|
|
248
|
+
plan: extractPlanTypeFromTokens(tokens),
|
|
239
249
|
refresh: tokens.refresh_token,
|
|
240
250
|
access: tokens.access_token,
|
|
241
251
|
expires: now + (tokens.expires_in ?? 3600) * 1000,
|
|
@@ -266,6 +276,7 @@ export async function refreshChatGptToken(auth) {
|
|
|
266
276
|
const json = (await response.json());
|
|
267
277
|
return {
|
|
268
278
|
...auth,
|
|
279
|
+
plan: extractPlanTypeFromTokens(json) ?? auth.plan,
|
|
269
280
|
access: json.access_token,
|
|
270
281
|
refresh: json.refresh_token ?? auth.refresh,
|
|
271
282
|
expires: Date.now() + (json.expires_in ?? 3600) * 1000,
|
|
@@ -276,19 +287,15 @@ export async function refreshChatGptToken(auth) {
|
|
|
276
287
|
}
|
|
277
288
|
}
|
|
278
289
|
/**
|
|
279
|
-
*
|
|
280
|
-
* Auto-refreshes if expired. Returns the token string or an Error.
|
|
281
|
-
* If the auth was refreshed, the `onRefresh` callback is called with
|
|
282
|
-
* the updated auth so the caller can persist it.
|
|
290
|
+
* Refresh the auth object if needed and return the full updated state.
|
|
283
291
|
*/
|
|
284
|
-
export async function
|
|
285
|
-
// Token still valid (with 60s buffer)
|
|
292
|
+
export async function getValidChatGptAuth(auth, onRefresh) {
|
|
286
293
|
if (auth.expires > Date.now() + 60_000) {
|
|
287
|
-
return auth
|
|
294
|
+
return auth;
|
|
288
295
|
}
|
|
289
296
|
const refreshed = await refreshChatGptToken(auth);
|
|
290
297
|
if (refreshed instanceof Error)
|
|
291
298
|
return refreshed;
|
|
292
299
|
onRefresh?.(refreshed);
|
|
293
|
-
return refreshed
|
|
300
|
+
return refreshed;
|
|
294
301
|
}
|
package/dist/cli.js
CHANGED
|
@@ -14,12 +14,14 @@ import { generateImage as aiGenerateImage, generateText, experimental_generateVi
|
|
|
14
14
|
import fs from 'node:fs';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import pc from 'picocolors';
|
|
17
|
+
import { createParser } from 'eventsource-parser';
|
|
17
18
|
import pkg from '../package.json' with { type: 'json' };
|
|
18
|
-
import { injectCredentialsToEnv, PROVIDERS } from './credentials.js';
|
|
19
|
-
import { IMAGE_MODELS, VIDEO_MODELS, DEFAULT_MODEL, DEFAULT_VIDEO_MODEL, getModelConfig, createImageModel, createTextModel, createVideoModel, } from './models.js';
|
|
19
|
+
import { injectCredentialsToEnv, PROVIDERS, getChatGptAuth, saveChatGptAuth, getKeyStatus, } from './credentials.js';
|
|
20
|
+
import { IMAGE_MODELS, VIDEO_MODELS, DEFAULT_MODEL, DEFAULT_VIDEO_MODEL, getModelConfig, createImageModel, createTextModel, createVideoModel, shouldUseResponsesApi, } from './models.js';
|
|
20
21
|
import { VIDEO_CATALOG } from './video-model-catalog.js';
|
|
21
22
|
import { loginInteractive, loginNonInteractive, showLoginStatus, removeLogin, readKeyFromStdin, } from './login.js';
|
|
22
23
|
import { subscribeInteractive, subscribeNonInteractive, unsubscribe, showUsage, } from './subscription.js';
|
|
24
|
+
import { getValidChatGptAuth } from './chatgpt-auth.js';
|
|
23
25
|
const cli = goke('egaki');
|
|
24
26
|
process.title = 'egaki';
|
|
25
27
|
// Print clean error output for unhandled rejections (e.g. AI SDK APICallError).
|
|
@@ -73,8 +75,13 @@ cli
|
|
|
73
75
|
}
|
|
74
76
|
// Non-interactive: --provider + --key or stdin
|
|
75
77
|
if (options.provider) {
|
|
78
|
+
// ChatGPT uses browser OAuth — skip key reading
|
|
79
|
+
if (options.provider === 'chatgpt') {
|
|
80
|
+
await loginNonInteractive({ provider: options.provider, key: '' });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
76
83
|
const key = options.key || (await readKeyFromStdin());
|
|
77
|
-
loginNonInteractive({ provider: options.provider, key });
|
|
84
|
+
await loginNonInteractive({ provider: options.provider, key });
|
|
78
85
|
return;
|
|
79
86
|
}
|
|
80
87
|
// Interactive mode
|
|
@@ -178,29 +185,52 @@ cli
|
|
|
178
185
|
.example('# Pipe to another tool')
|
|
179
186
|
.example('egaki image "logo design" --stdout | convert - -resize 512x512 logo.png')
|
|
180
187
|
.action(async (prompt, options) => {
|
|
181
|
-
// Inject stored API keys as env vars before calling the AI SDK
|
|
188
|
+
// Inject stored API keys as env vars before calling the AI SDK.
|
|
182
189
|
injectCredentialsToEnv();
|
|
183
|
-
|
|
184
|
-
|
|
190
|
+
// goke infers schema .default() values via Zod's input type, which leaves
|
|
191
|
+
// options.model / options.output / options.count as `T | undefined` even
|
|
192
|
+
// though the runtime always resolves them. Apply the same defaults here.
|
|
193
|
+
const model = options.model ?? DEFAULT_MODEL;
|
|
194
|
+
const outputPath = options.output ?? 'egaki-output.png';
|
|
195
|
+
const count = options.count ?? 1;
|
|
185
196
|
const config = getModelConfig(model);
|
|
197
|
+
const parsedAspectRatio = parseAspectRatioOption(options.aspectRatio);
|
|
186
198
|
if (!options.stdout) {
|
|
187
199
|
console.error(pc.dim(`Model: ${model}`));
|
|
188
200
|
console.error(pc.dim(`Prompt: ${prompt}`));
|
|
189
|
-
console.error(pc.dim(config.strategy === 'image'
|
|
190
|
-
? 'Mode: Image API (generateImage)'
|
|
191
|
-
: 'Mode: Text+Image (generateText)'));
|
|
192
201
|
}
|
|
193
202
|
const inputImages = await readInputImages(options.input);
|
|
194
203
|
const maskImage = options.mask
|
|
195
204
|
? await readInputSource(options.mask)
|
|
196
205
|
: undefined;
|
|
197
|
-
|
|
206
|
+
// When using ChatGPT OAuth with OpenAI image models, route through the
|
|
207
|
+
// Responses API + imageGeneration tool instead of the Image API.
|
|
208
|
+
// The Codex OAuth client lacks the `api.model.images.request` scope.
|
|
209
|
+
const useResponsesApi = config.strategy === 'image' && shouldUseResponsesApi(model);
|
|
210
|
+
if (useResponsesApi) {
|
|
211
|
+
if (!options.stdout) {
|
|
212
|
+
console.error(pc.dim('Mode: Responses API (ChatGPT OAuth)'));
|
|
213
|
+
}
|
|
214
|
+
await generateWithResponsesApi({
|
|
215
|
+
prompt,
|
|
216
|
+
model,
|
|
217
|
+
outputPath,
|
|
218
|
+
count,
|
|
219
|
+
aspectRatio: parsedAspectRatio,
|
|
220
|
+
seed: options.seed,
|
|
221
|
+
inputImages,
|
|
222
|
+
maskImage,
|
|
223
|
+
json: options.json || false,
|
|
224
|
+
stdout: options.stdout || false,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
else if (config.strategy === 'image') {
|
|
198
228
|
await generateWithImageModel({
|
|
199
229
|
prompt,
|
|
200
230
|
model,
|
|
201
231
|
outputPath,
|
|
202
|
-
count
|
|
203
|
-
aspectRatio:
|
|
232
|
+
count,
|
|
233
|
+
aspectRatio: parsedAspectRatio,
|
|
204
234
|
seed: options.seed,
|
|
205
235
|
inputImages,
|
|
206
236
|
maskImage,
|
|
@@ -261,8 +291,10 @@ cli
|
|
|
261
291
|
.example('egaki video "waves crashing on cliffs" -n 2 -o waves.mp4')
|
|
262
292
|
.action(async (prompt, options) => {
|
|
263
293
|
injectCredentialsToEnv();
|
|
264
|
-
|
|
265
|
-
const
|
|
294
|
+
// Apply the same .default(...) values goke's input-type inference drops.
|
|
295
|
+
const model = options.model ?? DEFAULT_VIDEO_MODEL;
|
|
296
|
+
const outputPath = options.output ?? 'egaki-output.mp4';
|
|
297
|
+
const count = options.count ?? 1;
|
|
266
298
|
const config = getModelConfig(model);
|
|
267
299
|
if (config.strategy !== 'video') {
|
|
268
300
|
console.error(pc.red(`Model ${model} is not a video model`));
|
|
@@ -280,7 +312,7 @@ cli
|
|
|
280
312
|
prompt,
|
|
281
313
|
model,
|
|
282
314
|
outputPath,
|
|
283
|
-
count
|
|
315
|
+
count,
|
|
284
316
|
aspectRatio: options.aspectRatio,
|
|
285
317
|
resolution: options.resolution,
|
|
286
318
|
duration: options.duration,
|
|
@@ -306,6 +338,7 @@ cli
|
|
|
306
338
|
.action(async (options) => {
|
|
307
339
|
const { CATALOG } = await import('./model-catalog.js');
|
|
308
340
|
const yaml = await import('js-yaml');
|
|
341
|
+
const providerStatuses = Object.fromEntries(Object.keys(PROVIDERS).map((provider) => [provider, getKeyStatus(provider)]));
|
|
309
342
|
let models = options.type === 'video'
|
|
310
343
|
? VIDEO_CATALOG
|
|
311
344
|
: options.type === 'image'
|
|
@@ -323,6 +356,7 @@ cli
|
|
|
323
356
|
name: m.name,
|
|
324
357
|
...(m.description ? { description: m.description } : {}),
|
|
325
358
|
provider: m.provider,
|
|
359
|
+
auth: providerStatuses[m.provider] ?? { available: false, source: 'none' },
|
|
326
360
|
strategy: m.strategy,
|
|
327
361
|
released: m.released,
|
|
328
362
|
cost: formatCatalogCost(m.cost),
|
|
@@ -529,6 +563,148 @@ async function generateWithTextModel({ prompt, model, outputPath, inputImages, i
|
|
|
529
563
|
console.log(JSON.stringify(output, null, 2));
|
|
530
564
|
}
|
|
531
565
|
}
|
|
566
|
+
// Generate using the OpenAI Responses API with imageGeneration tool.
|
|
567
|
+
// Used when auth comes from ChatGPT OAuth (no Image API scope).
|
|
568
|
+
async function generateWithResponsesApi({ prompt, model, outputPath, count, aspectRatio, seed, inputImages, maskImage, json, stdout, }) {
|
|
569
|
+
if (count !== 1) {
|
|
570
|
+
console.error(pc.red('ChatGPT image generation currently supports exactly one output image.'));
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
if (seed !== undefined) {
|
|
574
|
+
console.error(pc.red('ChatGPT image generation does not support --seed.'));
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
if (maskImage) {
|
|
578
|
+
console.error(pc.red('ChatGPT image generation does not support --mask yet.'));
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
const size = aspectRatio ? chatGptImageSizeFromAspectRatio(aspectRatio) : undefined;
|
|
582
|
+
if (aspectRatio && !size) {
|
|
583
|
+
console.error(pc.red('ChatGPT image generation only supports --aspect-ratio 1:1, 3:2, or 2:3.'));
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
const storedAuth = getChatGptAuth();
|
|
587
|
+
if (!storedAuth?.accountId) {
|
|
588
|
+
console.error(pc.red('Missing ChatGPT account metadata. Please run `egaki login --provider chatgpt` again.'));
|
|
589
|
+
process.exit(1);
|
|
590
|
+
}
|
|
591
|
+
const auth = await getValidChatGptAuth(storedAuth, saveChatGptAuth);
|
|
592
|
+
if (auth instanceof Error) {
|
|
593
|
+
console.error(pc.red(auth.message));
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
596
|
+
if (!stdout) {
|
|
597
|
+
console.error(pc.cyan('Generating...'));
|
|
598
|
+
}
|
|
599
|
+
const content = [
|
|
600
|
+
{ type: 'input_text', text: prompt },
|
|
601
|
+
...inputImages.map((image) => ({
|
|
602
|
+
type: 'input_image',
|
|
603
|
+
image_url: imageBytesToDataUrl(image),
|
|
604
|
+
})),
|
|
605
|
+
];
|
|
606
|
+
const response = await fetch('https://chatgpt.com/backend-api/codex/responses', {
|
|
607
|
+
method: 'POST',
|
|
608
|
+
headers: {
|
|
609
|
+
Authorization: `Bearer ${auth.access}`,
|
|
610
|
+
'ChatGPT-Account-ID': auth.accountId,
|
|
611
|
+
'Content-Type': 'application/json',
|
|
612
|
+
},
|
|
613
|
+
body: JSON.stringify({
|
|
614
|
+
model: 'gpt-5.4',
|
|
615
|
+
instructions: 'You are Codex.',
|
|
616
|
+
input: [
|
|
617
|
+
{
|
|
618
|
+
type: 'message',
|
|
619
|
+
role: 'user',
|
|
620
|
+
content,
|
|
621
|
+
},
|
|
622
|
+
],
|
|
623
|
+
tools: [
|
|
624
|
+
{
|
|
625
|
+
type: 'image_generation',
|
|
626
|
+
model,
|
|
627
|
+
size: 'auto',
|
|
628
|
+
quality: 'auto',
|
|
629
|
+
output_format: 'png',
|
|
630
|
+
output_compression: 100,
|
|
631
|
+
moderation: 'auto',
|
|
632
|
+
...(size ? { size } : {}),
|
|
633
|
+
},
|
|
634
|
+
],
|
|
635
|
+
tool_choice: 'auto',
|
|
636
|
+
parallel_tool_calls: true,
|
|
637
|
+
stream: true,
|
|
638
|
+
store: false,
|
|
639
|
+
include: [],
|
|
640
|
+
}),
|
|
641
|
+
});
|
|
642
|
+
if (!response.ok || !response.body) {
|
|
643
|
+
const body = await response.text().catch(() => '');
|
|
644
|
+
console.error(pc.red(`ChatGPT image generation failed: ${response.status}`));
|
|
645
|
+
if (body)
|
|
646
|
+
console.error(pc.dim(body));
|
|
647
|
+
process.exit(1);
|
|
648
|
+
}
|
|
649
|
+
let imageBase64;
|
|
650
|
+
let revisedPrompt = null;
|
|
651
|
+
const processSseMessage = (message) => {
|
|
652
|
+
if (!message.data)
|
|
653
|
+
return;
|
|
654
|
+
try {
|
|
655
|
+
const event = JSON.parse(message.data);
|
|
656
|
+
if (event.type === 'response.image_generation_call.partial_image' &&
|
|
657
|
+
event.partial_image_b64 &&
|
|
658
|
+
!imageBase64) {
|
|
659
|
+
imageBase64 = event.partial_image_b64;
|
|
660
|
+
}
|
|
661
|
+
if (event.type === 'response.output_item.done' &&
|
|
662
|
+
event.item?.type === 'image_generation_call') {
|
|
663
|
+
if (event.item.result)
|
|
664
|
+
imageBase64 = event.item.result;
|
|
665
|
+
revisedPrompt = event.item.revised_prompt ?? revisedPrompt;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
// Ignore keepalive and partial parse noise.
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
const parser = createParser({
|
|
673
|
+
onEvent: processSseMessage,
|
|
674
|
+
});
|
|
675
|
+
const decoder = new TextDecoder();
|
|
676
|
+
for await (const chunk of response.body) {
|
|
677
|
+
parser.feed(decoder.decode(chunk, { stream: true }));
|
|
678
|
+
}
|
|
679
|
+
parser.feed(decoder.decode());
|
|
680
|
+
if (!imageBase64) {
|
|
681
|
+
console.error(pc.red('No images generated.'));
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
const imageBytes = Buffer.from(imageBase64, 'base64');
|
|
685
|
+
const mediaType = 'image/png';
|
|
686
|
+
if (stdout) {
|
|
687
|
+
process.stdout.write(imageBytes);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const savedFiles = [];
|
|
691
|
+
const filePath = ensureExtension(outputPath, extensionFromMediaType(mediaType));
|
|
692
|
+
fs.writeFileSync(filePath, imageBytes);
|
|
693
|
+
console.error(pc.green(`Saved: ${filePath}`));
|
|
694
|
+
savedFiles.push(filePath);
|
|
695
|
+
if (revisedPrompt && !json) {
|
|
696
|
+
console.error(pc.dim(`Revised prompt: ${revisedPrompt}`));
|
|
697
|
+
}
|
|
698
|
+
if (json) {
|
|
699
|
+
const output = {
|
|
700
|
+
model,
|
|
701
|
+
files: savedFiles,
|
|
702
|
+
count: 1,
|
|
703
|
+
revisedPrompt,
|
|
704
|
+
};
|
|
705
|
+
console.log(JSON.stringify(output, null, 2));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
532
708
|
// Generate using experimental_generateVideo (video models)
|
|
533
709
|
async function generateWithVideoModel({ prompt, model, outputPath, count, aspectRatio, resolution, duration, fps, seed, inputImage, json, stdout, }) {
|
|
534
710
|
const videoModel = await createVideoModel(model);
|
|
@@ -648,6 +824,40 @@ function extensionFromMediaType(mediaType) {
|
|
|
648
824
|
};
|
|
649
825
|
return map[mediaType] || (mediaType.startsWith('video/') ? '.mp4' : '.png');
|
|
650
826
|
}
|
|
827
|
+
function inferImageMediaType(image) {
|
|
828
|
+
if (image.length >= 8 && image[0] === 0x89 && image[1] === 0x50 && image[2] === 0x4e && image[3] === 0x47) {
|
|
829
|
+
return 'image/png';
|
|
830
|
+
}
|
|
831
|
+
if (image.length >= 3 && image[0] === 0xff && image[1] === 0xd8 && image[2] === 0xff) {
|
|
832
|
+
return 'image/jpeg';
|
|
833
|
+
}
|
|
834
|
+
if (image.length >= 12 &&
|
|
835
|
+
image[0] === 0x52 && image[1] === 0x49 && image[2] === 0x46 && image[3] === 0x46 &&
|
|
836
|
+
image[8] === 0x57 && image[9] === 0x45 && image[10] === 0x42 && image[11] === 0x50) {
|
|
837
|
+
return 'image/webp';
|
|
838
|
+
}
|
|
839
|
+
if (image.length >= 6 &&
|
|
840
|
+
image[0] === 0x47 && image[1] === 0x49 && image[2] === 0x46 && image[3] === 0x38) {
|
|
841
|
+
return 'image/gif';
|
|
842
|
+
}
|
|
843
|
+
return 'image/png';
|
|
844
|
+
}
|
|
845
|
+
function imageBytesToDataUrl(image) {
|
|
846
|
+
const mediaType = inferImageMediaType(image);
|
|
847
|
+
return `data:${mediaType};base64,${Buffer.from(image).toString('base64')}`;
|
|
848
|
+
}
|
|
849
|
+
function chatGptImageSizeFromAspectRatio(aspectRatio) {
|
|
850
|
+
switch (aspectRatio) {
|
|
851
|
+
case '1:1':
|
|
852
|
+
return '1024x1024';
|
|
853
|
+
case '3:2':
|
|
854
|
+
return '1536x1024';
|
|
855
|
+
case '2:3':
|
|
856
|
+
return '1024x1536';
|
|
857
|
+
default:
|
|
858
|
+
return undefined;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
651
861
|
function normalizeResolutionKey(input) {
|
|
652
862
|
if (!input)
|
|
653
863
|
return undefined;
|
|
@@ -660,6 +870,15 @@ function normalizeResolutionKey(input) {
|
|
|
660
870
|
return '480p';
|
|
661
871
|
return normalized;
|
|
662
872
|
}
|
|
873
|
+
function isAspectRatio(input) {
|
|
874
|
+
return /^\d+:\d+$/.test(input);
|
|
875
|
+
}
|
|
876
|
+
function parseAspectRatioOption(input) {
|
|
877
|
+
if (!input || !isAspectRatio(input)) {
|
|
878
|
+
return undefined;
|
|
879
|
+
}
|
|
880
|
+
return input;
|
|
881
|
+
}
|
|
663
882
|
function ensureExtension(filePath, ext) {
|
|
664
883
|
const parsed = path.parse(filePath);
|
|
665
884
|
if (parsed.ext) {
|
package/dist/credentials.d.ts
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
import type { ChatGptAuth } from './chatgpt-auth.js';
|
|
2
|
+
export type ProviderInfo = {
|
|
2
3
|
envVar: string;
|
|
3
4
|
label: string;
|
|
4
5
|
hint: string;
|
|
5
|
-
|
|
6
|
+
/** If true, this provider uses OAuth instead of a plain API key */
|
|
7
|
+
oauth?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare const PROVIDERS: Record<string, ProviderInfo>;
|
|
6
10
|
export declare const EGAKI_GATEWAY_URL = "https://egaki.org/v3/ai";
|
|
7
|
-
|
|
11
|
+
/** A credential value is either a plain API key string or a ChatGPT OAuth object. */
|
|
12
|
+
export type CredentialValue = string | ChatGptAuth;
|
|
13
|
+
export type Credentials = Record<string, CredentialValue>;
|
|
8
14
|
export declare function readCredentials(): Credentials;
|
|
9
15
|
export declare function writeCredentials(creds: Credentials): void;
|
|
10
16
|
export declare function saveProviderKey(provider: string, key: string): void;
|
|
17
|
+
export declare function saveChatGptAuth(auth: ChatGptAuth): void;
|
|
18
|
+
export declare function getChatGptAuth(): ChatGptAuth | undefined;
|
|
11
19
|
export declare function removeProviderKey(provider: string): void;
|
|
12
20
|
export declare function getProviderKey(provider: string): string | undefined;
|
|
13
21
|
export declare function injectCredentialsToEnv(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Returns true when the current run should use the ChatGPT/Codex backend flow
|
|
24
|
+
* for OpenAI image models instead of a direct OpenAI API key.
|
|
25
|
+
*/
|
|
26
|
+
export declare function shouldUseChatGptBackend(): boolean;
|
|
14
27
|
export declare function getKeyStatus(provider: string): {
|
|
15
28
|
available: boolean;
|
|
16
|
-
source: 'env' | 'stored' | 'none';
|
|
29
|
+
source: 'env' | 'stored' | 'oauth' | 'none';
|
|
17
30
|
};
|
|
18
31
|
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAEpD,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,mEAAmE;IACnE,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AAKD,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAqClD,CAAA;AAED,eAAO,MAAM,iBAAiB,4BAA4B,CAAA;AAa1D,qFAAqF;AACrF,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,WAAW,CAAA;AAClD,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;AAEzD,wBAAgB,eAAe,IAAI,WAAW,CAO7C;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAOzD;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAInE;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAIvD;AAED,wBAAgB,cAAc,IAAI,WAAW,GAAG,SAAS,CAOxD;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIxD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAInE;AAKD,wBAAgB,sBAAsB,IAAI,IAAI,CAa7C;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,OAAO,CAEjD;AAID,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG;IAC9C,SAAS,EAAE,OAAO,CAAA;IAClB,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAA;CAC5C,CAuBA"}
|