egaki 0.2.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/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).
@@ -55,6 +57,7 @@ cli
55
57
  .example('egaki login')
56
58
  .example('# Non-interactive login with flags')
57
59
  .example('egaki login --provider google --key AIza...')
60
+ .example('egaki login --provider vertex --key AIza...')
58
61
  .example('# Pipe key from stdin (useful in CI/scripts)')
59
62
  .example('echo "AIza..." | egaki login --provider google')
60
63
  .example('# Show configured providers')
@@ -72,8 +75,13 @@ cli
72
75
  }
73
76
  // Non-interactive: --provider + --key or stdin
74
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
+ }
75
83
  const key = options.key || (await readKeyFromStdin());
76
- loginNonInteractive({ provider: options.provider, key });
84
+ await loginNonInteractive({ provider: options.provider, key });
77
85
  return;
78
86
  }
79
87
  // Interactive mode
@@ -170,34 +178,59 @@ cli
170
178
  .example('egaki image "fill with flowers" --input photo.jpg --mask mask.png')
171
179
  .example('# Generate with Gemini multimodal at 4K')
172
180
  .example('egaki image "dreamy landscape" -m gemini-2.5-flash-image --image-size 4K')
181
+ .example('# Route through Vertex AI (Google Cloud billing)')
182
+ .example('egaki image "product photo on marble" -m vertex/imagen-4.0-generate-001')
173
183
  .example('# Generate multiple images')
174
184
  .example('egaki image "abstract art" -n 4 -o art.png')
175
185
  .example('# Pipe to another tool')
176
186
  .example('egaki image "logo design" --stdout | convert - -resize 512x512 logo.png')
177
187
  .action(async (prompt, options) => {
178
- // 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.
179
189
  injectCredentialsToEnv();
180
- const model = options.model;
181
- const outputPath = options.output;
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;
182
196
  const config = getModelConfig(model);
197
+ const parsedAspectRatio = parseAspectRatioOption(options.aspectRatio);
183
198
  if (!options.stdout) {
184
199
  console.error(pc.dim(`Model: ${model}`));
185
200
  console.error(pc.dim(`Prompt: ${prompt}`));
186
- console.error(pc.dim(config.strategy === 'image'
187
- ? 'Mode: Image API (generateImage)'
188
- : 'Mode: Text+Image (generateText)'));
189
201
  }
190
202
  const inputImages = await readInputImages(options.input);
191
203
  const maskImage = options.mask
192
204
  ? await readInputSource(options.mask)
193
205
  : undefined;
194
- if (config.strategy === 'image') {
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') {
195
228
  await generateWithImageModel({
196
229
  prompt,
197
230
  model,
198
231
  outputPath,
199
- count: options.count,
200
- aspectRatio: options.aspectRatio,
232
+ count,
233
+ aspectRatio: parsedAspectRatio,
201
234
  seed: options.seed,
202
235
  inputImages,
203
236
  maskImage,
@@ -250,14 +283,18 @@ cli
250
283
  .example('egaki video "A paper airplane gliding through clouds" -o airplane.mp4')
251
284
  .example('# Generate with Veo model + duration')
252
285
  .example('egaki video "cinematic rainy street at night" -m veo-3.1-fast-generate-001 --duration 6')
286
+ .example('# Route through Vertex AI (Google Cloud billing)')
287
+ .example('egaki video "storm over mountains" -m vertex/veo-3.1-fast-generate-001 --duration 6')
253
288
  .example('# Image-to-video (model support required)')
254
289
  .example('egaki video "animate subtle camera pan" --model luma-ray-2 --input frame.png -o animated.mp4')
255
290
  .example('# Generate multiple videos')
256
291
  .example('egaki video "waves crashing on cliffs" -n 2 -o waves.mp4')
257
292
  .action(async (prompt, options) => {
258
293
  injectCredentialsToEnv();
259
- const model = options.model;
260
- const outputPath = options.output;
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;
261
298
  const config = getModelConfig(model);
262
299
  if (config.strategy !== 'video') {
263
300
  console.error(pc.red(`Model ${model} is not a video model`));
@@ -275,7 +312,7 @@ cli
275
312
  prompt,
276
313
  model,
277
314
  outputPath,
278
- count: options.count,
315
+ count,
279
316
  aspectRatio: options.aspectRatio,
280
317
  resolution: options.resolution,
281
318
  duration: options.duration,
@@ -301,6 +338,7 @@ cli
301
338
  .action(async (options) => {
302
339
  const { CATALOG } = await import('./model-catalog.js');
303
340
  const yaml = await import('js-yaml');
341
+ const providerStatuses = Object.fromEntries(Object.keys(PROVIDERS).map((provider) => [provider, getKeyStatus(provider)]));
304
342
  let models = options.type === 'video'
305
343
  ? VIDEO_CATALOG
306
344
  : options.type === 'image'
@@ -318,6 +356,7 @@ cli
318
356
  name: m.name,
319
357
  ...(m.description ? { description: m.description } : {}),
320
358
  provider: m.provider,
359
+ auth: providerStatuses[m.provider] ?? { available: false, source: 'none' },
321
360
  strategy: m.strategy,
322
361
  released: m.released,
323
362
  cost: formatCatalogCost(m.cost),
@@ -385,7 +424,10 @@ async function generateWithImageModel({ prompt, model, outputPath, count, aspect
385
424
  const imagePrompt = inputImages.length > 0
386
425
  ? { text: prompt, images: inputImages, ...(maskImage ? { mask: maskImage } : {}) }
387
426
  : prompt;
427
+ const config = getModelConfig(model);
388
428
  const imageModel = await createImageModel(model);
429
+ // Use the correct providerOptions key based on provider (google vs vertex)
430
+ const providerOptionsKey = config.provider === 'vertex' ? 'vertex' : 'google';
389
431
  if (!stdout) {
390
432
  console.error(pc.cyan('Generating...'));
391
433
  }
@@ -396,7 +438,7 @@ async function generateWithImageModel({ prompt, model, outputPath, count, aspect
396
438
  ...(aspectRatio ? { aspectRatio } : {}),
397
439
  ...(seed !== undefined ? { seed } : {}),
398
440
  providerOptions: {
399
- google: {
441
+ [providerOptionsKey]: {
400
442
  ...(allowPeople ? { personGeneration: 'allow_all' } : {}),
401
443
  ...(aspectRatio ? { aspectRatio } : {}),
402
444
  },
@@ -421,7 +463,6 @@ async function generateWithImageModel({ prompt, model, outputPath, count, aspect
421
463
  console.error(pc.green(`Saved: ${filePath}`));
422
464
  savedFiles.push(filePath);
423
465
  }
424
- const config = getModelConfig(model);
425
466
  const cost = calculateCost(config.cost, result.usage, result.images.length);
426
467
  if (cost != null) {
427
468
  console.error(pc.dim(`Cost: ${formatCost(cost)}`));
@@ -441,6 +482,8 @@ async function generateWithImageModel({ prompt, model, outputPath, count, aspect
441
482
  // Generate using generateText with responseModalities (Gemini multimodal models)
442
483
  async function generateWithTextModel({ prompt, model, outputPath, inputImages, imageSize, aspectRatio, json, stdout, }) {
443
484
  const textModel = await createTextModel(model);
485
+ const config = getModelConfig(model);
486
+ const providerOptionsKey = config.provider === 'vertex' ? 'vertex' : 'google';
444
487
  if (!stdout) {
445
488
  console.error(pc.cyan('Generating...'));
446
489
  }
@@ -463,7 +506,7 @@ async function generateWithTextModel({ prompt, model, outputPath, inputImages, i
463
506
  model: textModel,
464
507
  ...(messages ? { messages } : { prompt }),
465
508
  providerOptions: {
466
- google: {
509
+ [providerOptionsKey]: {
467
510
  responseModalities: ['TEXT', 'IMAGE'],
468
511
  ...(imageSize || aspectRatio
469
512
  ? {
@@ -501,7 +544,6 @@ async function generateWithTextModel({ prompt, model, outputPath, inputImages, i
501
544
  console.error(pc.green(`Saved: ${filePath}`));
502
545
  savedFiles.push(filePath);
503
546
  }
504
- const config = getModelConfig(model);
505
547
  const cost = calculateCost(config.cost, result.usage, imageFiles.length);
506
548
  if (cost != null) {
507
549
  console.error(pc.dim(`Cost: ${formatCost(cost)}`));
@@ -521,6 +563,148 @@ async function generateWithTextModel({ prompt, model, outputPath, inputImages, i
521
563
  console.log(JSON.stringify(output, null, 2));
522
564
  }
523
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
+ }
524
708
  // Generate using experimental_generateVideo (video models)
525
709
  async function generateWithVideoModel({ prompt, model, outputPath, count, aspectRatio, resolution, duration, fps, seed, inputImage, json, stdout, }) {
526
710
  const videoModel = await createVideoModel(model);
@@ -640,6 +824,40 @@ function extensionFromMediaType(mediaType) {
640
824
  };
641
825
  return map[mediaType] || (mediaType.startsWith('video/') ? '.mp4' : '.png');
642
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
+ }
643
861
  function normalizeResolutionKey(input) {
644
862
  if (!input)
645
863
  return undefined;
@@ -652,6 +870,15 @@ function normalizeResolutionKey(input) {
652
870
  return '480p';
653
871
  return normalized;
654
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
+ }
655
882
  function ensureExtension(filePath, ext) {
656
883
  const parsed = path.parse(filePath);
657
884
  if (parsed.ext) {
@@ -1,18 +1,31 @@
1
- export declare const PROVIDERS: Record<string, {
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
- export type Credentials = Record<string, string>;
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":"AAUA,eAAO,MAAM,SAAS,EAAE,MAAM,CAC5B,MAAM,EACN;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CA2BhD,CAAA;AAED,eAAO,MAAM,iBAAiB,4BAA4B,CAAA;AAa1D,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAEhD,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,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIxD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGnE;AAKD,wBAAgB,sBAAsB,IAAI,IAAI,CAW7C;AAID,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG;IAC9C,SAAS,EAAE,OAAO,CAAA;IAClB,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAA;CAClC,CAaA"}
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"}
@@ -1,6 +1,9 @@
1
1
  // Credential storage for egaki.
2
2
  // Reads and writes API keys from ~/.config/egaki/credentials.json.
3
3
  // Keys are stored per-provider and injected as env vars at runtime.
4
+ //
5
+ // Most providers store a plain API key string. The 'chatgpt' provider stores
6
+ // a structured ChatGptAuth object with OAuth tokens for the Codex backend flow.
4
7
  import fs from 'node:fs';
5
8
  import path from 'node:path';
6
9
  import os from 'node:os';
@@ -13,14 +16,25 @@ export const PROVIDERS = {
13
16
  label: 'Egaki (all models, one subscription)',
14
17
  hint: 'Subscribe at https://egaki.org/buy or run: egaki subscribe',
15
18
  },
19
+ chatgpt: {
20
+ envVar: 'OPENAI_API_KEY',
21
+ label: 'ChatGPT (use your subscription)',
22
+ hint: 'Sign in with your ChatGPT account via browser',
23
+ oauth: true,
24
+ },
16
25
  google: {
17
26
  envVar: 'GOOGLE_GENERATIVE_AI_API_KEY',
18
27
  label: 'Google AI (Gemini, Imagen)',
19
28
  hint: 'Get your key at https://aistudio.google.com/apikey',
20
29
  },
30
+ vertex: {
31
+ envVar: 'GOOGLE_VERTEX_API_KEY',
32
+ label: 'Google Vertex AI (Gemini, Imagen, Veo via Google Cloud)',
33
+ hint: 'Get an API key at https://console.cloud.google.com/apis/credentials',
34
+ },
21
35
  openai: {
22
36
  envVar: 'OPENAI_API_KEY',
23
- label: 'OpenAI (DALL-E)',
37
+ label: 'OpenAI (API key)',
24
38
  hint: 'Get your key at https://platform.openai.com/api-keys',
25
39
  },
26
40
  replicate: {
@@ -65,6 +79,19 @@ export function saveProviderKey(provider, key) {
65
79
  creds[provider] = key;
66
80
  writeCredentials(creds);
67
81
  }
82
+ export function saveChatGptAuth(auth) {
83
+ const creds = readCredentials();
84
+ creds['chatgpt'] = auth;
85
+ writeCredentials(creds);
86
+ }
87
+ export function getChatGptAuth() {
88
+ const creds = readCredentials();
89
+ const val = creds['chatgpt'];
90
+ if (val && typeof val === 'object') {
91
+ return val;
92
+ }
93
+ return undefined;
94
+ }
68
95
  export function removeProviderKey(provider) {
69
96
  const creds = readCredentials();
70
97
  delete creds[provider];
@@ -72,23 +99,33 @@ export function removeProviderKey(provider) {
72
99
  }
73
100
  export function getProviderKey(provider) {
74
101
  const creds = readCredentials();
75
- return creds[provider];
102
+ const val = creds[provider];
103
+ return typeof val === 'string' ? val : undefined;
76
104
  }
77
- // Inject all stored credentials as env vars so the AI SDK
78
- // providers pick them up automatically. Env vars already set
79
- // by the user take precedence (we don't overwrite them).
105
+ // Inject all stored non-ChatGPT credentials as env vars so the AI SDK
106
+ // providers pick them up automatically. Env vars already set by the user take
107
+ // precedence (we don't overwrite them).
80
108
  export function injectCredentialsToEnv() {
81
109
  const creds = readCredentials();
82
- for (const [provider, key] of Object.entries(creds)) {
110
+ for (const [provider, value] of Object.entries(creds)) {
83
111
  const info = PROVIDERS[provider];
84
- if (!info) {
112
+ if (!info)
85
113
  continue;
86
- }
87
- if (!process.env[info.envVar]) {
88
- process.env[info.envVar] = key;
114
+ // Skip ChatGPT OAuth entries — handled separately
115
+ if (provider === 'chatgpt')
116
+ continue;
117
+ if (typeof value === 'string' && !process.env[info.envVar]) {
118
+ process.env[info.envVar] = value;
89
119
  }
90
120
  }
91
121
  }
122
+ /**
123
+ * Returns true when the current run should use the ChatGPT/Codex backend flow
124
+ * for OpenAI image models instead of a direct OpenAI API key.
125
+ */
126
+ export function shouldUseChatGptBackend() {
127
+ return !process.env['OPENAI_API_KEY'] && Boolean(getChatGptAuth());
128
+ }
92
129
  // Check if a provider has a key available (from env or stored credentials).
93
130
  // Returns the source of the key for display purposes.
94
131
  export function getKeyStatus(provider) {
@@ -96,6 +133,14 @@ export function getKeyStatus(provider) {
96
133
  if (!info) {
97
134
  return { available: false, source: 'none' };
98
135
  }
136
+ // ChatGPT OAuth is a special case
137
+ if (provider === 'chatgpt') {
138
+ const auth = getChatGptAuth();
139
+ if (auth) {
140
+ return { available: true, source: 'oauth' };
141
+ }
142
+ return { available: false, source: 'none' };
143
+ }
99
144
  if (process.env[info.envVar]) {
100
145
  return { available: true, source: 'env' };
101
146
  }
package/dist/login.d.ts CHANGED
@@ -2,7 +2,7 @@ export declare function loginInteractive(): Promise<void>;
2
2
  export declare function loginNonInteractive({ provider, key, }: {
3
3
  provider: string;
4
4
  key: string;
5
- }): void;
5
+ }): Promise<void>;
6
6
  export declare function showLoginStatus(): void;
7
7
  export declare function removeLogin(provider: string): void;
8
8
  export declare function readKeyFromStdin(): Promise<string>;
@@ -1 +1 @@
1
- {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../src/login.ts"],"names":[],"mappings":"AAsBA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAqEtD;AAED,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,GAAG,GACJ,EAAE;IACD,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;CACZ,GAAG,IAAI,CAiBP;AAED,wBAAgB,eAAe,IAAI,IAAI,CAqBtC;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAYlD;AAGD,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC,CAMxD"}
1
+ {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../src/login.ts"],"names":[],"mappings":"AAyBA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAkFtD;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,CAyBhB;AAED,wBAAgB,eAAe,IAAI,IAAI,CAyCtC;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAalD;AAGD,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC,CAMxD"}