egaki 0.10.0 → 0.12.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 +83 -0
- package/dist/cli/chatgpt-auth.d.ts +30 -5
- package/dist/cli/chatgpt-auth.d.ts.map +1 -1
- package/dist/cli/chatgpt-auth.js +257 -79
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +97 -30
- package/dist/cli/generate.d.ts +10 -2
- package/dist/cli/generate.d.ts.map +1 -1
- package/dist/cli/generate.js +172 -59
- package/dist/cli/generate.test.js +50 -1
- package/dist/cli/login.d.ts +38 -3
- package/dist/cli/login.d.ts.map +1 -1
- package/dist/cli/login.js +44 -17
- package/dist/cli/login.test.d.ts +2 -0
- package/dist/cli/login.test.d.ts.map +1 -0
- package/dist/cli/login.test.js +158 -0
- package/dist/cli/model-catalog.d.ts.map +1 -1
- package/dist/cli/model-catalog.js +121 -5
- package/dist/cli/open-browser.d.ts +3 -1
- package/dist/cli/open-browser.d.ts.map +1 -1
- package/dist/cli/open-browser.js +2 -2
- package/dist/cli/xai-auth.d.ts +3 -1
- package/dist/cli/xai-auth.d.ts.map +1 -1
- package/dist/cli/xai-auth.js +13 -3
- package/dist/vite/generated-media-client.d.ts.map +1 -1
- package/dist/vite/generated-media-client.js +2 -2
- package/dist/vite/vite-plugin.d.ts.map +1 -1
- package/dist/vite/vite-plugin.js +20 -1
- package/package.json +7 -7
- package/skills/egaki/SKILL.md +14 -0
- package/src/cli/chatgpt-auth.ts +379 -89
- package/src/cli/cli.ts +115 -29
- package/src/cli/generate.test.ts +57 -0
- package/src/cli/generate.ts +198 -67
- package/src/cli/login.test.ts +185 -0
- package/src/cli/login.ts +68 -16
- package/src/cli/model-catalog.ts +128 -5
- package/src/cli/open-browser.ts +7 -6
- package/src/cli/xai-auth.ts +15 -3
- package/src/vite/generated-media-client.tsx +6 -1
- package/src/vite/vite-plugin.ts +20 -1
package/dist/cli/cli.js
CHANGED
|
@@ -7,19 +7,18 @@
|
|
|
7
7
|
// - imagen-* models → generateImage() with google.image()
|
|
8
8
|
// - all other models → generateText() with responseModalities: ['IMAGE']
|
|
9
9
|
// The CLI auto-detects which path to use based on model ID prefix.
|
|
10
|
-
import { goke } from 'goke';
|
|
10
|
+
import { colors as pc, goke, isAgent, } from 'goke';
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
import dedent from 'string-dedent';
|
|
13
13
|
import { APICallError, NoImageGeneratedError, NoVideoGeneratedError, RetryError, } from 'ai';
|
|
14
14
|
import { select, isCancel, cancel } from '@clack/prompts';
|
|
15
15
|
import fs from 'node:fs';
|
|
16
16
|
import path from 'node:path';
|
|
17
|
-
import pc from 'picocolors';
|
|
18
17
|
import pkg from '../../package.json' with { type: 'json' };
|
|
19
18
|
import { injectCredentialsToEnv, PROVIDERS, getKeyStatus, } from './credentials.js';
|
|
20
19
|
import { DEFAULT_MODEL, DEFAULT_VIDEO_MODEL, getModelConfig, } from './models.js';
|
|
21
20
|
import { CATALOG, VIDEO_CATALOG, describeProviderValues, } from './model-catalog.js';
|
|
22
|
-
import { loginInteractive, loginNonInteractive, showLoginStatus, removeLogin, readKeyFromStdin, } from './login.js';
|
|
21
|
+
import { loginInteractive, loginNonInteractive, resolveLoginAction, showLoginStatus, removeLogin, readKeyFromStdin, } from './login.js';
|
|
23
22
|
import { subscribeInteractive, subscribeNonInteractive, unsubscribe, showUsage, } from './subscription.js';
|
|
24
23
|
import { generateImageUncached as generateImage, generateVideoUncached as generateVideo, calculateCost, ValidationError, } from './generate.js';
|
|
25
24
|
import { generateSpeechUncached as generateSpeech, } from './speech-generate.js';
|
|
@@ -47,8 +46,8 @@ cli
|
|
|
47
46
|
.command('login', dedent `
|
|
48
47
|
Configure API keys for image generation providers.
|
|
49
48
|
Interactive mode: shows a provider picker and secure key input.
|
|
50
|
-
Non-interactive mode: pass
|
|
51
|
-
|
|
49
|
+
Non-interactive mode: pass \`--provider\` and \`--key\`, or pipe the key via stdin.
|
|
50
|
+
Browser OAuth runs in the background for AI agents and non-TTY shells.
|
|
52
51
|
`)
|
|
53
52
|
.option('-p, --provider [name]', z
|
|
54
53
|
.string()
|
|
@@ -67,29 +66,84 @@ cli
|
|
|
67
66
|
.example('egaki login --show')
|
|
68
67
|
.example('# Remove a stored key')
|
|
69
68
|
.example('egaki login --remove google')
|
|
70
|
-
.action(async (options) => {
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
.action(async (options, ctx) => {
|
|
70
|
+
const action = resolveLoginAction({
|
|
71
|
+
options,
|
|
72
|
+
daemonProvider: ctx.daemon.isDaemon
|
|
73
|
+
? ctx.process.env.EGAKI_LOGIN_PROVIDER
|
|
74
|
+
: undefined,
|
|
75
|
+
isAgent: isAgent || !process.stdin.isTTY,
|
|
76
|
+
});
|
|
77
|
+
if (action instanceof Error) {
|
|
78
|
+
ctx.console.error(action.message);
|
|
79
|
+
ctx.process.exit(1);
|
|
73
80
|
return;
|
|
74
81
|
}
|
|
75
|
-
if (
|
|
76
|
-
|
|
82
|
+
if (action.type === 'show') {
|
|
83
|
+
showLoginStatus({ loginRunning: await ctx.daemon.isRunning() });
|
|
77
84
|
return;
|
|
78
85
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
if (action.type === 'remove') {
|
|
87
|
+
removeLogin(action.provider);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (action.type === 'oauth-daemon') {
|
|
91
|
+
await loginNonInteractive({
|
|
92
|
+
provider: action.provider,
|
|
93
|
+
key: '',
|
|
94
|
+
background: true,
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (action.type === 'oauth-client') {
|
|
99
|
+
await startOAuthLogin({ provider: action.provider, ctx });
|
|
88
100
|
return;
|
|
89
101
|
}
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
if (action.type === 'api-key') {
|
|
103
|
+
await loginNonInteractive({
|
|
104
|
+
provider: action.provider,
|
|
105
|
+
key: action.key || (await readKeyFromStdin()),
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
await loginInteractive({
|
|
110
|
+
loginOAuth: (provider) => startOAuthLogin({ provider, ctx }),
|
|
111
|
+
});
|
|
92
112
|
});
|
|
113
|
+
cli
|
|
114
|
+
.command('login status <provider>', 'Check one provider login. Exits with status 1 when login is not complete.')
|
|
115
|
+
.example('egaki login status chatgpt')
|
|
116
|
+
.example('egaki login status xai-oauth')
|
|
117
|
+
.action(async (provider, _options, ctx) => {
|
|
118
|
+
const info = PROVIDERS[provider];
|
|
119
|
+
if (!info) {
|
|
120
|
+
ctx.console.error(`Unknown provider: ${provider}`);
|
|
121
|
+
ctx.process.exit(1);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const status = getKeyStatus(provider);
|
|
125
|
+
if (status.available) {
|
|
126
|
+
ctx.console.log(`${info.label} is configured.`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const loginRunning = await ctx.daemon.forCommand('login').isRunning();
|
|
130
|
+
ctx.console.error(loginRunning
|
|
131
|
+
? `An OAuth login is running, but ${info.label} is not configured yet.`
|
|
132
|
+
: `${info.label} is not configured.`);
|
|
133
|
+
ctx.process.exit(1);
|
|
134
|
+
});
|
|
135
|
+
async function startOAuthLogin({ provider, ctx, }) {
|
|
136
|
+
const runInBackground = isAgent || !process.stdin.isTTY;
|
|
137
|
+
await ctx.daemon.start({
|
|
138
|
+
attach: !runInBackground,
|
|
139
|
+
timeoutMs: 10 * 60 * 1000,
|
|
140
|
+
env: { EGAKI_LOGIN_PROVIDER: provider },
|
|
141
|
+
});
|
|
142
|
+
if (!runInBackground)
|
|
143
|
+
return;
|
|
144
|
+
ctx.console.log('Login is running in the background and the browser will open.');
|
|
145
|
+
ctx.console.log(`After approval, verify with: egaki login status ${provider}`);
|
|
146
|
+
}
|
|
93
147
|
// ─── subscribe command ───────────────────────────────────────────────────────
|
|
94
148
|
cli
|
|
95
149
|
.command('subscribe', dedent `
|
|
@@ -97,17 +151,18 @@ cli
|
|
|
97
151
|
You can also use your own provider keys (Google/OpenAI/Replicate/Fal)
|
|
98
152
|
via 'egaki login --provider <name> --key <key>' if you prefer BYOK.
|
|
99
153
|
Egaki subscription avoids managing one key per provider.
|
|
100
|
-
|
|
101
|
-
|
|
154
|
+
Two plans: Plus ($29/mo) and Pro ($99/mo). Each plan includes a
|
|
155
|
+
monthly dollar budget of generation usage equal to its price;
|
|
156
|
+
generations count their marked-up provider cost against the budget.
|
|
102
157
|
Interactive mode: pick a plan and get a checkout URL (email prefill optional).
|
|
103
158
|
Non-interactive: --email is optional and only pre-fills checkout.
|
|
104
159
|
`)
|
|
105
160
|
.option('-e, --email [email]', z.string().describe('Optional email prefill for checkout (skips interactive prompt)'))
|
|
106
|
-
.option('--plan [plan]', z.
|
|
161
|
+
.option('--plan [plan]', z.enum(['plus', 'pro']).describe('Plan ID: plus ($29/mo) or pro ($99/mo)'))
|
|
107
162
|
.example('# Interactive subscribe')
|
|
108
163
|
.example('egaki subscribe')
|
|
109
164
|
.example('# Non-interactive (for agents)')
|
|
110
|
-
.example('egaki subscribe --email user@example.com --plan
|
|
165
|
+
.example('egaki subscribe --email user@example.com --plan plus')
|
|
111
166
|
.example('# Non-interactive without email prefill')
|
|
112
167
|
.example('egaki subscribe --plan pro')
|
|
113
168
|
.action(async (options) => {
|
|
@@ -131,8 +186,8 @@ cli
|
|
|
131
186
|
// ─── usage command ───────────────────────────────────────────────────────────
|
|
132
187
|
cli
|
|
133
188
|
.command('usage', dedent `
|
|
134
|
-
Show your current Egaki
|
|
135
|
-
Displays plan,
|
|
189
|
+
Show your current Egaki usage for this billing period.
|
|
190
|
+
Displays plan, dollars spent, remaining budget, and period info.
|
|
136
191
|
`)
|
|
137
192
|
.example('egaki usage')
|
|
138
193
|
.action(async () => {
|
|
@@ -172,6 +227,15 @@ cli
|
|
|
172
227
|
console.log(` ${pc.dim('root:')} ${result.root}`);
|
|
173
228
|
console.log(` ${pc.dim('url:')} ${pc.cyan(result.url)}`);
|
|
174
229
|
console.log();
|
|
230
|
+
console.log(` ${pc.dim('SDK available in browser console as')} ${pc.yellow('window.egakiSDK')}`);
|
|
231
|
+
console.log(` ${pc.dim('.seekTo(frame)')} ${pc.dim('jump to a frame')}`);
|
|
232
|
+
console.log(` ${pc.dim('.screenshot()')} ${pc.dim('capture frame as data URL')}`);
|
|
233
|
+
console.log(` ${pc.dim('.filmstrip({...})')} ${pc.dim('grid of frames across scenes')}`);
|
|
234
|
+
console.log(` ${pc.dim('.export({...})')} ${pc.dim('render MP4 via WebCodecs')}`);
|
|
235
|
+
console.log(` ${pc.dim('.getInfo()')} ${pc.dim('composition metadata')}`);
|
|
236
|
+
console.log();
|
|
237
|
+
console.log(` ${pc.dim('Docs:')} ${pc.cyan('https://raw.githubusercontent.com/remorses/egaki/main/cli/skills/egaki/SKILL.md')}`);
|
|
238
|
+
console.log();
|
|
175
239
|
if (options.open) {
|
|
176
240
|
const { openUrlInBrowser } = await import('./open-browser.js');
|
|
177
241
|
await openUrlInBrowser(result.url);
|
|
@@ -194,7 +258,7 @@ cli
|
|
|
194
258
|
.option('-n, --count [n]', z.number().default(1).describe('Number of images to generate'))
|
|
195
259
|
.option('--aspect-ratio [ratio]', z
|
|
196
260
|
.string()
|
|
197
|
-
.describe('Aspect ratio for the generated image. Imagen supports: 1:1, 3:4, 4:3, 9:16, 16:9. Gemini supports additional ratios: 2:3, 3:2, 4:5, 5:4, 21:9'))
|
|
261
|
+
.describe('Aspect ratio for the generated image. Imagen supports: 1:1, 3:4, 4:3, 9:16, 16:9. Gemini supports additional ratios: 2:3, 3:2, 4:5, 5:4, 21:9. OpenAI models (gpt-image-*, dall-e-*) have no native aspect ratio parameter, so the ratio is snapped to the closest supported size and stated in the prompt text'))
|
|
198
262
|
.option('--seed [seed]', z.number().describe('Seed for reproducible generation. Same seed + same prompt = same image'))
|
|
199
263
|
.option('--image-size [size]', z
|
|
200
264
|
.enum(['1K', '2K', '4K'])
|
|
@@ -320,7 +384,7 @@ cli
|
|
|
320
384
|
.describe(`Video operation mode. ${describeProviderValues('mode', [VIDEO_CATALOG])}`))
|
|
321
385
|
.option('--reference-images [path]', z
|
|
322
386
|
.array(z.string())
|
|
323
|
-
.describe('Reference images for R2V generation, repeatable. xAI: 1-7 images. Seedance: 1-9 images (use @Image1 etc. in prompt)'))
|
|
387
|
+
.describe('Reference images for R2V generation, repeatable. xAI: 1-7 images (1-14 on grok-imagine-video-1.5). Seedance: 1-9 images (use @Image1 etc. in prompt)'))
|
|
324
388
|
.option('--negative-prompt [text]', z
|
|
325
389
|
.string()
|
|
326
390
|
.describe('Describe what to avoid in the generated video (Fal models)'))
|
|
@@ -1012,6 +1076,7 @@ const UPLOAD_GATEWAY_BASE = 'https://egaki.org';
|
|
|
1012
1076
|
/** Max upload size in bytes (must match gateway limit). */
|
|
1013
1077
|
const MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
|
|
1014
1078
|
cli.help();
|
|
1079
|
+
cli.completions();
|
|
1015
1080
|
cli.version(pkg.version);
|
|
1016
1081
|
export { cli };
|
|
1017
1082
|
async function runProviderCall(operation, call) {
|
|
@@ -1158,6 +1223,8 @@ const FEATURED_IMAGE_MODELS = [
|
|
|
1158
1223
|
'gemini-2.5-flash-image',
|
|
1159
1224
|
'gemini-3.1-flash-image-preview',
|
|
1160
1225
|
'nano-banana-pro-preview',
|
|
1226
|
+
'gpt-image-2.5-sunburst',
|
|
1227
|
+
'gpt-image-2.5-flare',
|
|
1161
1228
|
'gpt-image-2',
|
|
1162
1229
|
'gpt-image-1.5',
|
|
1163
1230
|
'chatgpt-image-latest',
|
|
@@ -1166,7 +1233,7 @@ const FEATURED_IMAGE_MODELS = [
|
|
|
1166
1233
|
'flux-pro-1.1-ultra',
|
|
1167
1234
|
'recraft-v4',
|
|
1168
1235
|
'dall-e-3',
|
|
1169
|
-
'grok-imagine-image-
|
|
1236
|
+
'grok-imagine-image-2.0',
|
|
1170
1237
|
];
|
|
1171
1238
|
/**
|
|
1172
1239
|
* Resolve the image model: show interactive picker in TTY, use default otherwise.
|
package/dist/cli/generate.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ValidationError } from './models.js';
|
|
2
2
|
export { ValidationError };
|
|
3
3
|
/** Image model IDs from the catalog. Accepts arbitrary strings too. */
|
|
4
|
-
export type ImageModelId = 'imagen-4.0-generate-001' | 'imagen-4.0-ultra-generate-001' | 'imagen-4.0-fast-generate-001' | 'gemini-2.0-flash-exp-image-generation' | 'gemini-2.5-flash-image' | 'gemini-3-pro-image-preview' | 'gemini-3.1-flash-image-preview' | 'vertex/imagen-4.0-generate-001' | 'vertex/imagen-4.0-ultra-generate-001' | 'vertex/imagen-4.0-fast-generate-001' | 'vertex/gemini-2.5-flash-image' | 'vertex/gemini-3-pro-image-preview' | 'vertex/gemini-3.1-flash-image-preview' | 'flux-kontext-max' | 'flux-kontext-pro' | 'flux-pro-1.0-fill' | 'flux-pro-1.1' | 'flux-pro-1.1-ultra' | 'flux-2-pro' | 'flux-2-max' | 'flux-2-flex' | 'flux-2-klein-9b' | 'flux-2-klein-4b' | 'recraft-v2' | 'recraft-v3' | 'recraft-v4' | 'recraft-v4-pro' | 'recraft-v4.1' | 'recraft-v4.1-pro' | 'recraft-v4.1-utility' | 'recraft-v4.1-utility-pro' | 'seedream-5.0-lite' | 'seedream-4.5' | 'seedream-4.0' | 'grok-imagine-image' | 'grok-imagine-image-quality' | 'dall-e-2' | 'dall-e-3' | 'gpt-image-1' | 'gpt-image-1-mini' | 'gpt-image-1.5' | 'gpt-image-2' | 'chatgpt-image-latest' | (string & {});
|
|
4
|
+
export type ImageModelId = 'imagen-4.0-generate-001' | 'imagen-4.0-ultra-generate-001' | 'imagen-4.0-fast-generate-001' | 'gemini-2.0-flash-exp-image-generation' | 'gemini-2.5-flash-image' | 'gemini-3-pro-image-preview' | 'gemini-3.1-flash-image-preview' | 'vertex/imagen-4.0-generate-001' | 'vertex/imagen-4.0-ultra-generate-001' | 'vertex/imagen-4.0-fast-generate-001' | 'vertex/gemini-2.5-flash-image' | 'vertex/gemini-3-pro-image-preview' | 'vertex/gemini-3.1-flash-image-preview' | 'flux-kontext-max' | 'flux-kontext-pro' | 'flux-pro-1.0-fill' | 'flux-pro-1.1' | 'flux-pro-1.1-ultra' | 'flux-2-pro' | 'flux-2-max' | 'flux-2-flex' | 'flux-2-klein-9b' | 'flux-2-klein-4b' | 'recraft-v2' | 'recraft-v3' | 'recraft-v4' | 'recraft-v4-pro' | 'recraft-v4.1' | 'recraft-v4.1-pro' | 'recraft-v4.1-utility' | 'recraft-v4.1-utility-pro' | 'seedream-5.0-lite' | 'seedream-4.5' | 'seedream-4.0' | 'grok-imagine-image' | 'grok-imagine-image-2.0' | 'grok-imagine-image-quality' | 'dall-e-2' | 'dall-e-3' | 'gpt-image-1' | 'gpt-image-1-mini' | 'gpt-image-1.5' | 'gpt-image-2' | 'gpt-image-2.5-flare' | 'gpt-image-2.5-sunburst' | 'chatgpt-image-latest' | (string & {});
|
|
5
5
|
/** Video model IDs from the catalog. Accepts arbitrary strings too. */
|
|
6
|
-
export type VideoModelId = 'veo-3.1-generate-001' | 'veo-3.1-fast-generate-001' | 'veo-3.0-generate-001' | 'veo-3.0-fast-generate-001' | 'vertex/veo-3.1-generate-001' | 'vertex/veo-3.1-fast-generate-001' | 'grok-imagine-video' | 'grok-imagine-video-1.5' | 'kling-v3.0-t2v' | 'kling-v3.0-i2v' | 'kling-v3.0-motion-control' | 'kling-v2.6-t2v' | 'kling-v2.6-i2v' | 'kling-v2.6-motion-control' | 'kling-v2.5-turbo-t2v' | 'kling-v2.5-turbo-i2v' | 'wan-v2.6-t2v' | 'wan-v2.6-i2v' | 'wan-v2.6-i2v-flash' | 'wan-v2.6-r2v' | 'wan-v2.6-r2v-flash' | 'wan-v2.5-t2v-preview' | 'seedance-2.0' | 'seedance-2.0-fast' | 'seedance-v1.5-pro' | 'seedance-v1.0-pro' | 'seedance-v1.0-pro-fast' | 'seedance-v1.0-lite-t2v' | 'seedance-v1.0-lite-i2v' | 'luma-ray-2' | 'minimax-video' | 'hunyuan-video' | (string & {});
|
|
6
|
+
export type VideoModelId = 'veo-3.1-generate-001' | 'veo-3.1-fast-generate-001' | 'veo-3.0-generate-001' | 'veo-3.0-fast-generate-001' | 'vertex/veo-3.1-generate-001' | 'vertex/veo-3.1-fast-generate-001' | 'grok-imagine-video' | 'grok-imagine-video-1.5' | 'grok-imagine-video-1.5-lite' | 'kling-v3.0-t2v' | 'kling-v3.0-i2v' | 'kling-v3.0-motion-control' | 'kling-v2.6-t2v' | 'kling-v2.6-i2v' | 'kling-v2.6-motion-control' | 'kling-v2.5-turbo-t2v' | 'kling-v2.5-turbo-i2v' | 'wan-v2.6-t2v' | 'wan-v2.6-i2v' | 'wan-v2.6-i2v-flash' | 'wan-v2.6-r2v' | 'wan-v2.6-r2v-flash' | 'wan-v2.5-t2v-preview' | 'seedance-2.0' | 'seedance-2.0-fast' | 'seedance-v1.5-pro' | 'seedance-v1.0-pro' | 'seedance-v1.0-pro-fast' | 'seedance-v1.0-lite-t2v' | 'seedance-v1.0-lite-i2v' | 'luma-ray-2' | 'minimax-video' | 'hunyuan-video' | (string & {});
|
|
7
7
|
/** Common aspect ratios across providers. */
|
|
8
8
|
export type AspectRatio = '1:1' | '3:4' | '4:3' | '9:16' | '16:9' | '2:3' | '3:2' | '4:5' | '5:4' | '9:21' | '21:9' | (string & {});
|
|
9
9
|
/** Image quality presets (varies by provider). */
|
|
@@ -114,6 +114,14 @@ export declare function calculateCost(cost: {
|
|
|
114
114
|
resolution?: string;
|
|
115
115
|
hasVideoInput?: boolean;
|
|
116
116
|
}, count?: number): number | null;
|
|
117
|
+
/**
|
|
118
|
+
* Translate an aspect ratio request into a prompt prefix + concrete `size` for
|
|
119
|
+
* models that have no native aspect ratio parameter.
|
|
120
|
+
*/
|
|
121
|
+
export declare function aspectRatioFallback(prompt: string, aspectRatio: `${number}:${number}` | undefined, sizes: readonly string[]): {
|
|
122
|
+
prompt: string;
|
|
123
|
+
size?: `${number}x${number}`;
|
|
124
|
+
};
|
|
117
125
|
/**
|
|
118
126
|
* Generate images from a text prompt. Auto-detects which generation strategy
|
|
119
127
|
* to use (image API, text model, or ChatGPT Responses API) based on model ID.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/cli/generate.ts"],"names":[],"mappings":"AAgCA,OAAO,EAQL,eAAe,EAChB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,eAAe,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/cli/generate.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/generate.js
CHANGED
|
@@ -19,7 +19,7 @@ import { createParser } from 'eventsource-parser';
|
|
|
19
19
|
import { injectCredentialsToEnv, getChatGptAuth, saveChatGptAuth, } from './credentials.js';
|
|
20
20
|
import { getModelConfig, createImageModel, createTextModel, createVideoModel, shouldUseResponsesApi, DEFAULT_MODEL, DEFAULT_VIDEO_MODEL, ValidationError, } from './models.js';
|
|
21
21
|
export { ValidationError };
|
|
22
|
-
import { getValidChatGptAuth } from './chatgpt-auth.js';
|
|
22
|
+
import { chatGptReLoginMessage, getValidChatGptAuth, isRevokedChatGptAccessToken, refreshChatGptToken, resolveChatGptResponsesModel, } from './chatgpt-auth.js';
|
|
23
23
|
export function buildImageProviderOptions(provider, opts) {
|
|
24
24
|
switch (provider) {
|
|
25
25
|
case 'google': {
|
|
@@ -38,7 +38,8 @@ export function buildImageProviderOptions(provider, opts) {
|
|
|
38
38
|
}
|
|
39
39
|
case 'xai': {
|
|
40
40
|
const xaiOpts = {
|
|
41
|
-
|
|
41
|
+
// 'auto' is xAI's server default; @ai-sdk/xai does not accept it, so omit the field.
|
|
42
|
+
...(opts.quality && opts.quality !== 'auto' ? { quality: opts.quality } : {}),
|
|
42
43
|
...(opts.resolution ? { resolution: opts.resolution } : {}),
|
|
43
44
|
...(opts.outputFormat ? { output_format: opts.outputFormat } : {}),
|
|
44
45
|
};
|
|
@@ -181,21 +182,70 @@ function imageBytesToDataUrl(image) {
|
|
|
181
182
|
const mediaType = inferImageMediaType(image);
|
|
182
183
|
return `data:${mediaType};base64,${Buffer.from(image).toString('base64')}`;
|
|
183
184
|
}
|
|
184
|
-
function chatGptImageSizeFromAspectRatio(aspectRatio) {
|
|
185
|
-
switch (aspectRatio) {
|
|
186
|
-
case '1:1':
|
|
187
|
-
return '1024x1024';
|
|
188
|
-
case '3:2':
|
|
189
|
-
return '1536x1024';
|
|
190
|
-
case '2:3':
|
|
191
|
-
return '1024x1536';
|
|
192
|
-
default:
|
|
193
|
-
return undefined;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
185
|
function isAspectRatio(input) {
|
|
197
186
|
return /^\d+:\d+$/.test(input);
|
|
198
187
|
}
|
|
188
|
+
// ─── aspect ratio fallback for models without native support ─────────────────
|
|
189
|
+
//
|
|
190
|
+
// OpenAI image models (gpt-image-*, dall-e-*) reject `aspectRatio`: the AI SDK
|
|
191
|
+
// pushes an "unsupported: aspectRatio" warning and the request falls back to a
|
|
192
|
+
// square image. They accept a fixed `size` list instead, and they also honour a
|
|
193
|
+
// plain-language ratio statement in the prompt text when picking the framing.
|
|
194
|
+
// So for these models we do both: snap to the closest supported size AND state
|
|
195
|
+
// the ratio in the prompt. The prompt hint matters because the supported sizes
|
|
196
|
+
// are coarse (only 3:2 / 1:1 / 2:3), so a request like 16:9 has no exact match
|
|
197
|
+
// and the composition has to come from the prompt.
|
|
198
|
+
//
|
|
199
|
+
// Models with a non-empty `features.aspectRatios` keep the native flag.
|
|
200
|
+
const OPENAI_SIZES = ['1024x1024', '1536x1024', '1024x1536'];
|
|
201
|
+
function ratioValue(aspectRatio) {
|
|
202
|
+
const [w, h] = aspectRatio.split(':').map(Number);
|
|
203
|
+
if (!w || !h)
|
|
204
|
+
return undefined;
|
|
205
|
+
return w / h;
|
|
206
|
+
}
|
|
207
|
+
function orientationLabel(ratio) {
|
|
208
|
+
if (ratio > 1.02)
|
|
209
|
+
return 'landscape';
|
|
210
|
+
if (ratio < 0.98)
|
|
211
|
+
return 'portrait';
|
|
212
|
+
return 'square';
|
|
213
|
+
}
|
|
214
|
+
/** Pick the supported WIDTHxHEIGHT whose ratio is closest to the requested one. */
|
|
215
|
+
function closestSize(aspectRatio, sizes) {
|
|
216
|
+
const target = ratioValue(aspectRatio);
|
|
217
|
+
if (target === undefined || sizes.length === 0)
|
|
218
|
+
return undefined;
|
|
219
|
+
let best;
|
|
220
|
+
let bestDistance = Infinity;
|
|
221
|
+
for (const size of sizes) {
|
|
222
|
+
const [w, h] = size.split('x').map(Number);
|
|
223
|
+
if (!w || !h)
|
|
224
|
+
continue;
|
|
225
|
+
// Compare in log space so 2x-too-wide and 2x-too-tall are equally far off.
|
|
226
|
+
const distance = Math.abs(Math.log(w / h) - Math.log(target));
|
|
227
|
+
if (distance < bestDistance) {
|
|
228
|
+
bestDistance = distance;
|
|
229
|
+
best = size;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return best;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Translate an aspect ratio request into a prompt prefix + concrete `size` for
|
|
236
|
+
* models that have no native aspect ratio parameter.
|
|
237
|
+
*/
|
|
238
|
+
export function aspectRatioFallback(prompt, aspectRatio, sizes) {
|
|
239
|
+
if (!aspectRatio)
|
|
240
|
+
return { prompt };
|
|
241
|
+
const ratio = ratioValue(aspectRatio);
|
|
242
|
+
if (ratio === undefined)
|
|
243
|
+
return { prompt };
|
|
244
|
+
return {
|
|
245
|
+
prompt: `Image aspect ratio: ${aspectRatio}, ${orientationLabel(ratio)} orientation. ${prompt}`,
|
|
246
|
+
size: closestSize(aspectRatio, sizes),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
199
249
|
/** Convert any thrown value into an Error instance. */
|
|
200
250
|
function toError(err) {
|
|
201
251
|
if (err instanceof Error)
|
|
@@ -336,12 +386,19 @@ export async function generateVideoUncached(opts) {
|
|
|
336
386
|
}
|
|
337
387
|
// ─── internal generation paths ───────────────────────────────────────────────
|
|
338
388
|
async function generateWithImageModel(opts) {
|
|
339
|
-
const imagePrompt = opts.inputImages.length > 0
|
|
340
|
-
? { text: opts.prompt, images: opts.inputImages, ...(opts.maskImage ? { mask: opts.maskImage } : {}) }
|
|
341
|
-
: opts.prompt;
|
|
342
389
|
const config = getModelConfig(opts.model);
|
|
343
390
|
if (config instanceof Error)
|
|
344
391
|
return config;
|
|
392
|
+
// Models with an empty `aspectRatios` list have no native aspect ratio
|
|
393
|
+
// parameter, so the request is expressed through `size` + the prompt text.
|
|
394
|
+
const features = config.strategy === 'video' ? undefined : config.features;
|
|
395
|
+
const supportsNativeAspectRatio = (features?.aspectRatios?.length ?? 0) > 0;
|
|
396
|
+
const fallback = supportsNativeAspectRatio
|
|
397
|
+
? { prompt: opts.prompt, size: undefined }
|
|
398
|
+
: aspectRatioFallback(opts.prompt, opts.aspectRatio, features?.sizes ?? []);
|
|
399
|
+
const imagePrompt = opts.inputImages.length > 0
|
|
400
|
+
? { text: fallback.prompt, images: opts.inputImages, ...(opts.maskImage ? { mask: opts.maskImage } : {}) }
|
|
401
|
+
: fallback.prompt;
|
|
345
402
|
const imageModel = await createImageModel(opts.model);
|
|
346
403
|
if (imageModel instanceof Error)
|
|
347
404
|
return imageModel;
|
|
@@ -359,7 +416,8 @@ async function generateWithImageModel(opts) {
|
|
|
359
416
|
model: imageModel,
|
|
360
417
|
prompt: imagePrompt,
|
|
361
418
|
n: opts.count,
|
|
362
|
-
...(opts.aspectRatio ? { aspectRatio: opts.aspectRatio } : {}),
|
|
419
|
+
...(opts.aspectRatio && supportsNativeAspectRatio ? { aspectRatio: opts.aspectRatio } : {}),
|
|
420
|
+
...(fallback.size ? { size: fallback.size } : {}),
|
|
363
421
|
...(opts.seed !== undefined ? { seed: opts.seed } : {}),
|
|
364
422
|
providerOptions,
|
|
365
423
|
});
|
|
@@ -453,10 +511,7 @@ async function generateWithResponsesApi(opts) {
|
|
|
453
511
|
if (opts.maskImage) {
|
|
454
512
|
return new ValidationError('ChatGPT image generation does not support mask yet.');
|
|
455
513
|
}
|
|
456
|
-
const size = opts.
|
|
457
|
-
if (opts.aspectRatio && !size) {
|
|
458
|
-
return new ValidationError('ChatGPT image generation only supports aspect ratios 1:1, 3:2, or 2:3.');
|
|
459
|
-
}
|
|
514
|
+
const { prompt, size } = aspectRatioFallback(opts.prompt, opts.aspectRatio, OPENAI_SIZES);
|
|
460
515
|
const storedAuth = getChatGptAuth();
|
|
461
516
|
if (!storedAuth?.accountId) {
|
|
462
517
|
return new ValidationError('Missing ChatGPT account metadata. Please run `egaki login --provider chatgpt` again.');
|
|
@@ -465,49 +520,62 @@ async function generateWithResponsesApi(opts) {
|
|
|
465
520
|
if (auth instanceof Error)
|
|
466
521
|
return auth;
|
|
467
522
|
const content = [
|
|
468
|
-
{ type: 'input_text', text:
|
|
523
|
+
{ type: 'input_text', text: prompt },
|
|
469
524
|
...opts.inputImages.map((image) => ({
|
|
470
525
|
type: 'input_image',
|
|
471
526
|
image_url: imageBytesToDataUrl(image),
|
|
472
527
|
})),
|
|
473
528
|
];
|
|
529
|
+
const responsesModel = await resolveChatGptResponsesModel({ auth });
|
|
530
|
+
if (responsesModel instanceof Error)
|
|
531
|
+
return responsesModel;
|
|
532
|
+
const requestBodyFor = (model) => JSON.stringify({
|
|
533
|
+
model,
|
|
534
|
+
instructions: 'You are Codex.',
|
|
535
|
+
input: [
|
|
536
|
+
{
|
|
537
|
+
type: 'message',
|
|
538
|
+
role: 'user',
|
|
539
|
+
content,
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
tools: [
|
|
543
|
+
{
|
|
544
|
+
type: 'image_generation',
|
|
545
|
+
model: opts.model,
|
|
546
|
+
quality: 'auto',
|
|
547
|
+
output_format: 'png',
|
|
548
|
+
output_compression: 100,
|
|
549
|
+
moderation: 'auto',
|
|
550
|
+
size: size ?? 'auto',
|
|
551
|
+
},
|
|
552
|
+
],
|
|
553
|
+
tool_choice: 'auto',
|
|
554
|
+
parallel_tool_calls: true,
|
|
555
|
+
stream: true,
|
|
556
|
+
store: false,
|
|
557
|
+
include: [],
|
|
558
|
+
});
|
|
559
|
+
const post = ({ access, accountId, body, }) => {
|
|
560
|
+
const headers = new Headers({
|
|
561
|
+
Authorization: `Bearer ${access}`,
|
|
562
|
+
'Content-Type': 'application/json',
|
|
563
|
+
});
|
|
564
|
+
if (accountId)
|
|
565
|
+
headers.set('ChatGPT-Account-ID', accountId);
|
|
566
|
+
return fetch('https://chatgpt.com/backend-api/codex/responses', {
|
|
567
|
+
method: 'POST',
|
|
568
|
+
headers,
|
|
569
|
+
body,
|
|
570
|
+
});
|
|
571
|
+
};
|
|
572
|
+
let activeModel = responsesModel;
|
|
474
573
|
let response;
|
|
475
574
|
try {
|
|
476
|
-
response = await
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
...(auth.accountId && { 'ChatGPT-Account-ID': auth.accountId }),
|
|
481
|
-
'Content-Type': 'application/json',
|
|
482
|
-
},
|
|
483
|
-
body: JSON.stringify({
|
|
484
|
-
model: 'gpt-5.4',
|
|
485
|
-
instructions: 'You are Codex.',
|
|
486
|
-
input: [
|
|
487
|
-
{
|
|
488
|
-
type: 'message',
|
|
489
|
-
role: 'user',
|
|
490
|
-
content,
|
|
491
|
-
},
|
|
492
|
-
],
|
|
493
|
-
tools: [
|
|
494
|
-
{
|
|
495
|
-
type: 'image_generation',
|
|
496
|
-
model: opts.model,
|
|
497
|
-
size: 'auto',
|
|
498
|
-
quality: 'auto',
|
|
499
|
-
output_format: 'png',
|
|
500
|
-
output_compression: 100,
|
|
501
|
-
moderation: 'auto',
|
|
502
|
-
...(size ? { size } : {}),
|
|
503
|
-
},
|
|
504
|
-
],
|
|
505
|
-
tool_choice: 'auto',
|
|
506
|
-
parallel_tool_calls: true,
|
|
507
|
-
stream: true,
|
|
508
|
-
store: false,
|
|
509
|
-
include: [],
|
|
510
|
-
}),
|
|
575
|
+
response = await post({
|
|
576
|
+
access: auth.access,
|
|
577
|
+
accountId: auth.accountId,
|
|
578
|
+
body: requestBodyFor(activeModel),
|
|
511
579
|
});
|
|
512
580
|
}
|
|
513
581
|
catch (err) {
|
|
@@ -515,7 +583,52 @@ async function generateWithResponsesApi(opts) {
|
|
|
515
583
|
}
|
|
516
584
|
if (!response.ok || !response.body) {
|
|
517
585
|
const body = await response.text().catch(() => '');
|
|
518
|
-
|
|
586
|
+
if (!(response.status === 400 && body.includes('not supported'))) {
|
|
587
|
+
if (!isRevokedChatGptAccessToken(response.status, body)) {
|
|
588
|
+
return new Error(`ChatGPT image generation failed: ${response.status}${body ? ` ${body}` : ''}`);
|
|
589
|
+
}
|
|
590
|
+
const refreshed = await refreshChatGptToken(auth);
|
|
591
|
+
if (refreshed instanceof Error)
|
|
592
|
+
return refreshed;
|
|
593
|
+
saveChatGptAuth(refreshed);
|
|
594
|
+
try {
|
|
595
|
+
response = await post({
|
|
596
|
+
access: refreshed.access,
|
|
597
|
+
accountId: refreshed.accountId,
|
|
598
|
+
body: requestBodyFor(activeModel),
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
catch (err) {
|
|
602
|
+
return toError(err);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
const nextModel = await resolveChatGptResponsesModel({
|
|
607
|
+
auth,
|
|
608
|
+
exclude: [activeModel],
|
|
609
|
+
});
|
|
610
|
+
if (nextModel instanceof Error) {
|
|
611
|
+
return new Error(`ChatGPT image generation failed: ${response.status} ${body}`);
|
|
612
|
+
}
|
|
613
|
+
activeModel = nextModel;
|
|
614
|
+
try {
|
|
615
|
+
response = await post({
|
|
616
|
+
access: auth.access,
|
|
617
|
+
accountId: auth.accountId,
|
|
618
|
+
body: requestBodyFor(activeModel),
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
catch (err) {
|
|
622
|
+
return toError(err);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (!response.ok || !response.body) {
|
|
626
|
+
const retryBody = await response.text().catch(() => '');
|
|
627
|
+
if (isRevokedChatGptAccessToken(response.status, retryBody)) {
|
|
628
|
+
return new Error(chatGptReLoginMessage({ status: response.status, body: retryBody }));
|
|
629
|
+
}
|
|
630
|
+
return new Error(`ChatGPT image generation failed: ${response.status}${retryBody ? ` ${retryBody}` : ''}`);
|
|
631
|
+
}
|
|
519
632
|
}
|
|
520
633
|
let imageBase64;
|
|
521
634
|
let revisedPrompt = null;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Tests the error-as-value pattern and validation logic without
|
|
3
3
|
// making actual API calls (no credentials needed).
|
|
4
4
|
import { describe, it, expect } from 'vitest';
|
|
5
|
-
import { generateImage, generateVideo, calculateCost, buildImageProviderOptions, buildVideoProviderOptions, ValidationError, } from './generate.js';
|
|
5
|
+
import { generateImage, generateVideo, calculateCost, buildImageProviderOptions, buildVideoProviderOptions, aspectRatioFallback, ValidationError, } from './generate.js';
|
|
6
6
|
describe('generateImage', () => {
|
|
7
7
|
it('returns ValidationError for unknown model', async () => {
|
|
8
8
|
const result = await generateImage({
|
|
@@ -110,6 +110,55 @@ describe('buildImageProviderOptions', () => {
|
|
|
110
110
|
expect(opts).toEqual({});
|
|
111
111
|
});
|
|
112
112
|
});
|
|
113
|
+
describe('aspectRatioFallback', () => {
|
|
114
|
+
const openaiSizes = ['1024x1024', '1536x1024', '1024x1536'];
|
|
115
|
+
it('leaves the prompt untouched when no aspect ratio is requested', () => {
|
|
116
|
+
expect(aspectRatioFallback('a sunset', undefined, openaiSizes)).toMatchInlineSnapshot(`
|
|
117
|
+
{
|
|
118
|
+
"prompt": "a sunset",
|
|
119
|
+
}
|
|
120
|
+
`);
|
|
121
|
+
});
|
|
122
|
+
it('prepends the ratio and picks the closest landscape size', () => {
|
|
123
|
+
expect(aspectRatioFallback('a sunset', '16:9', openaiSizes)).toMatchInlineSnapshot(`
|
|
124
|
+
{
|
|
125
|
+
"prompt": "Image aspect ratio: 16:9, landscape orientation. a sunset",
|
|
126
|
+
"size": "1536x1024",
|
|
127
|
+
}
|
|
128
|
+
`);
|
|
129
|
+
});
|
|
130
|
+
it('picks the closest portrait size', () => {
|
|
131
|
+
expect(aspectRatioFallback('a sunset', '9:16', openaiSizes)).toMatchInlineSnapshot(`
|
|
132
|
+
{
|
|
133
|
+
"prompt": "Image aspect ratio: 9:16, portrait orientation. a sunset",
|
|
134
|
+
"size": "1024x1536",
|
|
135
|
+
}
|
|
136
|
+
`);
|
|
137
|
+
});
|
|
138
|
+
it('picks the square size for 1:1', () => {
|
|
139
|
+
expect(aspectRatioFallback('a sunset', '1:1', openaiSizes)).toMatchInlineSnapshot(`
|
|
140
|
+
{
|
|
141
|
+
"prompt": "Image aspect ratio: 1:1, square orientation. a sunset",
|
|
142
|
+
"size": "1024x1024",
|
|
143
|
+
}
|
|
144
|
+
`);
|
|
145
|
+
});
|
|
146
|
+
it('still prepends the ratio when the model has no size list', () => {
|
|
147
|
+
expect(aspectRatioFallback('a sunset', '21:9', [])).toMatchInlineSnapshot(`
|
|
148
|
+
{
|
|
149
|
+
"prompt": "Image aspect ratio: 21:9, landscape orientation. a sunset",
|
|
150
|
+
"size": undefined,
|
|
151
|
+
}
|
|
152
|
+
`);
|
|
153
|
+
});
|
|
154
|
+
it('ignores malformed ratios', () => {
|
|
155
|
+
expect(aspectRatioFallback('a sunset', '0:0', openaiSizes)).toMatchInlineSnapshot(`
|
|
156
|
+
{
|
|
157
|
+
"prompt": "a sunset",
|
|
158
|
+
}
|
|
159
|
+
`);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
113
162
|
describe('buildVideoProviderOptions', () => {
|
|
114
163
|
it('builds xai edit-video options', () => {
|
|
115
164
|
const opts = buildVideoProviderOptions('xai', {
|