egaki 0.10.0 → 0.11.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/cli/chatgpt-auth.d.ts +30 -5
  3. package/dist/cli/chatgpt-auth.d.ts.map +1 -1
  4. package/dist/cli/chatgpt-auth.js +257 -79
  5. package/dist/cli/cli.d.ts.map +1 -1
  6. package/dist/cli/cli.js +93 -28
  7. package/dist/cli/generate.d.ts +8 -0
  8. package/dist/cli/generate.d.ts.map +1 -1
  9. package/dist/cli/generate.js +170 -58
  10. package/dist/cli/generate.test.js +50 -1
  11. package/dist/cli/login.d.ts +38 -3
  12. package/dist/cli/login.d.ts.map +1 -1
  13. package/dist/cli/login.js +44 -17
  14. package/dist/cli/login.test.d.ts +2 -0
  15. package/dist/cli/login.test.d.ts.map +1 -0
  16. package/dist/cli/login.test.js +158 -0
  17. package/dist/cli/open-browser.d.ts +3 -1
  18. package/dist/cli/open-browser.d.ts.map +1 -1
  19. package/dist/cli/open-browser.js +2 -2
  20. package/dist/cli/xai-auth.d.ts +3 -1
  21. package/dist/cli/xai-auth.d.ts.map +1 -1
  22. package/dist/cli/xai-auth.js +13 -3
  23. package/dist/vite/generated-media-client.d.ts.map +1 -1
  24. package/dist/vite/generated-media-client.js +2 -2
  25. package/dist/vite/vite-plugin.d.ts.map +1 -1
  26. package/dist/vite/vite-plugin.js +20 -1
  27. package/package.json +6 -6
  28. package/skills/egaki/SKILL.md +14 -0
  29. package/src/cli/chatgpt-auth.ts +379 -89
  30. package/src/cli/cli.ts +111 -27
  31. package/src/cli/generate.test.ts +57 -0
  32. package/src/cli/generate.ts +191 -61
  33. package/src/cli/login.test.ts +185 -0
  34. package/src/cli/login.ts +68 -16
  35. package/src/cli/open-browser.ts +7 -6
  36. package/src/cli/xai-auth.ts +15 -3
  37. package/src/vite/generated-media-client.tsx +6 -1
  38. 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 --provider and --key flags, or pipe key via stdin.
51
- Keys are saved to ~/.config/egaki/credentials.json (mode 0600).
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
- if (options.show) {
72
- showLoginStatus();
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 (options.remove) {
76
- removeLogin(options.remove);
82
+ if (action.type === 'show') {
83
+ showLoginStatus({ loginRunning: await ctx.daemon.isRunning() });
77
84
  return;
78
85
  }
79
- // Non-interactive: --provider + --key or stdin
80
- if (options.provider) {
81
- // OAuth providers use browser flow — skip key reading
82
- if (options.provider === 'chatgpt' || options.provider === 'xai-oauth') {
83
- await loginNonInteractive({ provider: options.provider, key: '' });
84
- return;
85
- }
86
- const key = options.key || (await readKeyFromStdin());
87
- await loginNonInteractive({ provider: options.provider, key });
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
- // Interactive mode
91
- await loginInteractive();
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
- Three plans: Starter ($9/mo, 100 credits), Pro ($29/mo, 500 credits),
101
- Unlimited ($99/mo, 2000 credits). One credit ≈ one standard image.
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.string().describe('Plan ID: starter, pro, or unlimited (default: pro)'))
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 pro')
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 credit usage for this billing period.
135
- Displays plan, credits used, credits remaining, and period info.
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'])
@@ -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) {
@@ -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":"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"}
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/cli/generate.ts"],"names":[],"mappings":"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"}
@@ -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': {
@@ -181,21 +181,70 @@ function imageBytesToDataUrl(image) {
181
181
  const mediaType = inferImageMediaType(image);
182
182
  return `data:${mediaType};base64,${Buffer.from(image).toString('base64')}`;
183
183
  }
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
184
  function isAspectRatio(input) {
197
185
  return /^\d+:\d+$/.test(input);
198
186
  }
187
+ // ─── aspect ratio fallback for models without native support ─────────────────
188
+ //
189
+ // OpenAI image models (gpt-image-*, dall-e-*) reject `aspectRatio`: the AI SDK
190
+ // pushes an "unsupported: aspectRatio" warning and the request falls back to a
191
+ // square image. They accept a fixed `size` list instead, and they also honour a
192
+ // plain-language ratio statement in the prompt text when picking the framing.
193
+ // So for these models we do both: snap to the closest supported size AND state
194
+ // the ratio in the prompt. The prompt hint matters because the supported sizes
195
+ // are coarse (only 3:2 / 1:1 / 2:3), so a request like 16:9 has no exact match
196
+ // and the composition has to come from the prompt.
197
+ //
198
+ // Models with a non-empty `features.aspectRatios` keep the native flag.
199
+ const OPENAI_SIZES = ['1024x1024', '1536x1024', '1024x1536'];
200
+ function ratioValue(aspectRatio) {
201
+ const [w, h] = aspectRatio.split(':').map(Number);
202
+ if (!w || !h)
203
+ return undefined;
204
+ return w / h;
205
+ }
206
+ function orientationLabel(ratio) {
207
+ if (ratio > 1.02)
208
+ return 'landscape';
209
+ if (ratio < 0.98)
210
+ return 'portrait';
211
+ return 'square';
212
+ }
213
+ /** Pick the supported WIDTHxHEIGHT whose ratio is closest to the requested one. */
214
+ function closestSize(aspectRatio, sizes) {
215
+ const target = ratioValue(aspectRatio);
216
+ if (target === undefined || sizes.length === 0)
217
+ return undefined;
218
+ let best;
219
+ let bestDistance = Infinity;
220
+ for (const size of sizes) {
221
+ const [w, h] = size.split('x').map(Number);
222
+ if (!w || !h)
223
+ continue;
224
+ // Compare in log space so 2x-too-wide and 2x-too-tall are equally far off.
225
+ const distance = Math.abs(Math.log(w / h) - Math.log(target));
226
+ if (distance < bestDistance) {
227
+ bestDistance = distance;
228
+ best = size;
229
+ }
230
+ }
231
+ return best;
232
+ }
233
+ /**
234
+ * Translate an aspect ratio request into a prompt prefix + concrete `size` for
235
+ * models that have no native aspect ratio parameter.
236
+ */
237
+ export function aspectRatioFallback(prompt, aspectRatio, sizes) {
238
+ if (!aspectRatio)
239
+ return { prompt };
240
+ const ratio = ratioValue(aspectRatio);
241
+ if (ratio === undefined)
242
+ return { prompt };
243
+ return {
244
+ prompt: `Image aspect ratio: ${aspectRatio}, ${orientationLabel(ratio)} orientation. ${prompt}`,
245
+ size: closestSize(aspectRatio, sizes),
246
+ };
247
+ }
199
248
  /** Convert any thrown value into an Error instance. */
200
249
  function toError(err) {
201
250
  if (err instanceof Error)
@@ -336,12 +385,19 @@ export async function generateVideoUncached(opts) {
336
385
  }
337
386
  // ─── internal generation paths ───────────────────────────────────────────────
338
387
  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
388
  const config = getModelConfig(opts.model);
343
389
  if (config instanceof Error)
344
390
  return config;
391
+ // Models with an empty `aspectRatios` list have no native aspect ratio
392
+ // parameter, so the request is expressed through `size` + the prompt text.
393
+ const features = config.strategy === 'video' ? undefined : config.features;
394
+ const supportsNativeAspectRatio = (features?.aspectRatios?.length ?? 0) > 0;
395
+ const fallback = supportsNativeAspectRatio
396
+ ? { prompt: opts.prompt, size: undefined }
397
+ : aspectRatioFallback(opts.prompt, opts.aspectRatio, features?.sizes ?? []);
398
+ const imagePrompt = opts.inputImages.length > 0
399
+ ? { text: fallback.prompt, images: opts.inputImages, ...(opts.maskImage ? { mask: opts.maskImage } : {}) }
400
+ : fallback.prompt;
345
401
  const imageModel = await createImageModel(opts.model);
346
402
  if (imageModel instanceof Error)
347
403
  return imageModel;
@@ -359,7 +415,8 @@ async function generateWithImageModel(opts) {
359
415
  model: imageModel,
360
416
  prompt: imagePrompt,
361
417
  n: opts.count,
362
- ...(opts.aspectRatio ? { aspectRatio: opts.aspectRatio } : {}),
418
+ ...(opts.aspectRatio && supportsNativeAspectRatio ? { aspectRatio: opts.aspectRatio } : {}),
419
+ ...(fallback.size ? { size: fallback.size } : {}),
363
420
  ...(opts.seed !== undefined ? { seed: opts.seed } : {}),
364
421
  providerOptions,
365
422
  });
@@ -453,10 +510,7 @@ async function generateWithResponsesApi(opts) {
453
510
  if (opts.maskImage) {
454
511
  return new ValidationError('ChatGPT image generation does not support mask yet.');
455
512
  }
456
- const size = opts.aspectRatio ? chatGptImageSizeFromAspectRatio(opts.aspectRatio) : undefined;
457
- if (opts.aspectRatio && !size) {
458
- return new ValidationError('ChatGPT image generation only supports aspect ratios 1:1, 3:2, or 2:3.');
459
- }
513
+ const { prompt, size } = aspectRatioFallback(opts.prompt, opts.aspectRatio, OPENAI_SIZES);
460
514
  const storedAuth = getChatGptAuth();
461
515
  if (!storedAuth?.accountId) {
462
516
  return new ValidationError('Missing ChatGPT account metadata. Please run `egaki login --provider chatgpt` again.');
@@ -465,49 +519,62 @@ async function generateWithResponsesApi(opts) {
465
519
  if (auth instanceof Error)
466
520
  return auth;
467
521
  const content = [
468
- { type: 'input_text', text: opts.prompt },
522
+ { type: 'input_text', text: prompt },
469
523
  ...opts.inputImages.map((image) => ({
470
524
  type: 'input_image',
471
525
  image_url: imageBytesToDataUrl(image),
472
526
  })),
473
527
  ];
528
+ const responsesModel = await resolveChatGptResponsesModel({ auth });
529
+ if (responsesModel instanceof Error)
530
+ return responsesModel;
531
+ const requestBodyFor = (model) => JSON.stringify({
532
+ model,
533
+ instructions: 'You are Codex.',
534
+ input: [
535
+ {
536
+ type: 'message',
537
+ role: 'user',
538
+ content,
539
+ },
540
+ ],
541
+ tools: [
542
+ {
543
+ type: 'image_generation',
544
+ model: opts.model,
545
+ quality: 'auto',
546
+ output_format: 'png',
547
+ output_compression: 100,
548
+ moderation: 'auto',
549
+ size: size ?? 'auto',
550
+ },
551
+ ],
552
+ tool_choice: 'auto',
553
+ parallel_tool_calls: true,
554
+ stream: true,
555
+ store: false,
556
+ include: [],
557
+ });
558
+ const post = ({ access, accountId, body, }) => {
559
+ const headers = new Headers({
560
+ Authorization: `Bearer ${access}`,
561
+ 'Content-Type': 'application/json',
562
+ });
563
+ if (accountId)
564
+ headers.set('ChatGPT-Account-ID', accountId);
565
+ return fetch('https://chatgpt.com/backend-api/codex/responses', {
566
+ method: 'POST',
567
+ headers,
568
+ body,
569
+ });
570
+ };
571
+ let activeModel = responsesModel;
474
572
  let response;
475
573
  try {
476
- response = await fetch('https://chatgpt.com/backend-api/codex/responses', {
477
- method: 'POST',
478
- headers: {
479
- Authorization: `Bearer ${auth.access}`,
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
- }),
574
+ response = await post({
575
+ access: auth.access,
576
+ accountId: auth.accountId,
577
+ body: requestBodyFor(activeModel),
511
578
  });
512
579
  }
513
580
  catch (err) {
@@ -515,7 +582,52 @@ async function generateWithResponsesApi(opts) {
515
582
  }
516
583
  if (!response.ok || !response.body) {
517
584
  const body = await response.text().catch(() => '');
518
- return new Error(`ChatGPT image generation failed: ${response.status}${body ? ` ${body}` : ''}`);
585
+ if (!(response.status === 400 && body.includes('not supported'))) {
586
+ if (!isRevokedChatGptAccessToken(response.status, body)) {
587
+ return new Error(`ChatGPT image generation failed: ${response.status}${body ? ` ${body}` : ''}`);
588
+ }
589
+ const refreshed = await refreshChatGptToken(auth);
590
+ if (refreshed instanceof Error)
591
+ return refreshed;
592
+ saveChatGptAuth(refreshed);
593
+ try {
594
+ response = await post({
595
+ access: refreshed.access,
596
+ accountId: refreshed.accountId,
597
+ body: requestBodyFor(activeModel),
598
+ });
599
+ }
600
+ catch (err) {
601
+ return toError(err);
602
+ }
603
+ }
604
+ else {
605
+ const nextModel = await resolveChatGptResponsesModel({
606
+ auth,
607
+ exclude: [activeModel],
608
+ });
609
+ if (nextModel instanceof Error) {
610
+ return new Error(`ChatGPT image generation failed: ${response.status} ${body}`);
611
+ }
612
+ activeModel = nextModel;
613
+ try {
614
+ response = await post({
615
+ access: auth.access,
616
+ accountId: auth.accountId,
617
+ body: requestBodyFor(activeModel),
618
+ });
619
+ }
620
+ catch (err) {
621
+ return toError(err);
622
+ }
623
+ }
624
+ if (!response.ok || !response.body) {
625
+ const retryBody = await response.text().catch(() => '');
626
+ if (isRevokedChatGptAccessToken(response.status, retryBody)) {
627
+ return new Error(chatGptReLoginMessage({ status: response.status, body: retryBody }));
628
+ }
629
+ return new Error(`ChatGPT image generation failed: ${response.status}${retryBody ? ` ${retryBody}` : ''}`);
630
+ }
519
631
  }
520
632
  let imageBase64;
521
633
  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', {
@@ -1,9 +1,44 @@
1
- export declare function loginInteractive(): Promise<void>;
2
- export declare function loginNonInteractive({ provider, key, }: {
1
+ type LoginAction = {
2
+ type: 'show';
3
+ } | {
4
+ type: 'remove';
5
+ provider: string;
6
+ } | {
7
+ type: 'oauth-daemon';
8
+ provider: string;
9
+ } | {
10
+ type: 'oauth-client';
11
+ provider: string;
12
+ } | {
13
+ type: 'api-key';
14
+ provider: string;
15
+ key: string | undefined;
16
+ } | {
17
+ type: 'interactive';
18
+ };
19
+ export declare function resolveLoginAction({ options, daemonProvider, isAgent, }: {
20
+ options: {
21
+ provider?: string;
22
+ key?: string;
23
+ show?: boolean;
24
+ remove?: string;
25
+ };
26
+ daemonProvider: string | undefined;
27
+ isAgent: boolean;
28
+ }): LoginAction | Error;
29
+ export declare function loginInteractive({ loginOAuth, }: {
30
+ loginOAuth: (provider: string) => Promise<void>;
31
+ }): Promise<void>;
32
+ export declare function loginNonInteractive({ provider, key, background, }: {
3
33
  provider: string;
4
34
  key: string;
35
+ background?: boolean;
5
36
  }): Promise<void>;
6
- export declare function showLoginStatus(): void;
37
+ export declare function describeChatGptExpiry(expires: number, now: number): string;
38
+ export declare function showLoginStatus({ loginRunning }?: {
39
+ loginRunning?: boolean | undefined;
40
+ }): void;
7
41
  export declare function removeLogin(provider: string): void;
8
42
  export declare function readKeyFromStdin(): Promise<string>;
43
+ export {};
9
44
  //# sourceMappingURL=login.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/cli/login.ts"],"names":[],"mappings":"AA4BA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAkGtD;AAED,wBAAsB,mBAAmB,CAAC,EACxC,QAAQ,EACR,GAAG,GACJ,EAAE;IACD,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;CACZ,GAAG,OAAO,CAAC,IAAI,CAAC,CAiChB;AAED,wBAAgB,eAAe,IAAI,IAAI,CA2DtC;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAalD;AAGD,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC,CAMxD"}
1
+ {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/cli/login.ts"],"names":[],"mappings":"AA4BA,KAAK,WAAW,GACZ;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GAC9D;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAA;AAM3B,wBAAgB,kBAAkB,CAAC,EACjC,OAAO,EACP,cAAc,EACd,OAAO,GACR,EAAE;IACD,OAAO,EAAE;QACP,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,GAAG,CAAC,EAAE,MAAM,CAAA;QACZ,IAAI,CAAC,EAAE,OAAO,CAAA;QACd,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAA;IACD,cAAc,EAAE,MAAM,GAAG,SAAS,CAAA;IAClC,OAAO,EAAE,OAAO,CAAA;CACjB,GAAG,WAAW,GAAG,KAAK,CAiBtB;AAED,wBAAsB,gBAAgB,CAAC,EACrC,UAAU,GACX,EAAE;IACD,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAChD,GAAG,OAAO,CAAC,IAAI,CAAC,CA8FhB;AAED,wBAAsB,mBAAmB,CAAC,EACxC,QAAQ,EACR,GAAG,EACH,UAAkB,GACnB,EAAE;IACD,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiChB;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAG1E;AAED,wBAAgB,eAAe,CAAC,EAAE,YAAoB,EAAE;;CAAK,GAAG,IAAI,CA4DnE;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAalD;AAGD,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC,CAMxD"}