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/src/login.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // Login command — interactive and non-interactive API key management.
2
2
  // Interactive mode uses clack prompts for provider selection and key input.
3
3
  // Non-interactive mode supports --provider + --key flags and stdin piping.
4
+ // ChatGPT OAuth uses a browser flow instead of pasting a key.
4
5
  import {
5
6
  intro,
6
7
  outro,
@@ -17,8 +18,10 @@ import {
17
18
  saveProviderKey,
18
19
  removeProviderKey,
19
20
  getKeyStatus,
20
- readCredentials,
21
+ getChatGptAuth,
22
+ saveChatGptAuth,
21
23
  } from './credentials.js'
24
+ import { chatGptOAuthLogin, extractPlanType } from './chatgpt-auth.js'
22
25
 
23
26
  export async function loginInteractive(): Promise<void> {
24
27
  intro(pc.bold('egaki login'))
@@ -32,6 +35,11 @@ export async function loginInteractive(): Promise<void> {
32
35
  if (status.source === 'stored') {
33
36
  return pc.green('(saved)')
34
37
  }
38
+ if (status.source === 'oauth') {
39
+ const auth = getChatGptAuth()
40
+ const email = auth?.email
41
+ return pc.green(`(signed in${email ? ` as ${email}` : ''})`)
42
+ }
35
43
  return pc.dim('(not configured)')
36
44
  })()
37
45
  // Highlight the egaki option as recommended
@@ -61,6 +69,14 @@ export async function loginInteractive(): Promise<void> {
61
69
  process.exit(1)
62
70
  }
63
71
 
72
+ // ChatGPT uses browser OAuth instead of key paste
73
+ if (provider === 'chatgpt') {
74
+ const auth = await chatGptOAuthLogin()
75
+ saveChatGptAuth(auth)
76
+ outro('Done — ChatGPT OAuth saved')
77
+ return
78
+ }
79
+
64
80
  log.info(
65
81
  `${pc.dim('Env var:')} ${info.envVar} ${pc.dim('(also used by the CLI when set)')}`,
66
82
  )
@@ -91,13 +107,13 @@ export async function loginInteractive(): Promise<void> {
91
107
  outro('Done')
92
108
  }
93
109
 
94
- export function loginNonInteractive({
110
+ export async function loginNonInteractive({
95
111
  provider,
96
112
  key,
97
113
  }: {
98
114
  provider: string
99
115
  key: string
100
- }): void {
116
+ }): Promise<void> {
101
117
  const info = PROVIDERS[provider]
102
118
  if (!info) {
103
119
  const available = Object.keys(PROVIDERS).join(', ')
@@ -107,6 +123,14 @@ export function loginNonInteractive({
107
123
  process.exit(1)
108
124
  }
109
125
 
126
+ // ChatGPT uses browser OAuth — key flag is not needed
127
+ if (provider === 'chatgpt') {
128
+ const auth = await chatGptOAuthLogin()
129
+ saveChatGptAuth(auth)
130
+ console.log(pc.green('ChatGPT OAuth saved'))
131
+ return
132
+ }
133
+
110
134
  if (!key || key.trim().length === 0) {
111
135
  console.error(pc.red('API key cannot be empty'))
112
136
  process.exit(1)
@@ -121,6 +145,26 @@ export function showLoginStatus(): void {
121
145
 
122
146
  for (const [key, info] of Object.entries(PROVIDERS)) {
123
147
  const status = getKeyStatus(key)
148
+
149
+ if (key === 'chatgpt') {
150
+ const auth = getChatGptAuth()
151
+ if (auth) {
152
+ const plan = extractPlanType(auth)
153
+ const email = auth.email ?? auth.accountId ?? 'unknown'
154
+ const expired = auth.expires < Date.now()
155
+ const expiryLabel = expired
156
+ ? pc.yellow('(token expired, will auto-refresh)')
157
+ : pc.dim(`(token valid)`)
158
+ console.log(`${pc.green('*')} ${info.label} ${pc.green('(signed in)')}`)
159
+ console.log(pc.dim(` account: ${email}${plan ? `, plan: ${plan}` : ''}`))
160
+ console.log(` ${expiryLabel}`)
161
+ } else {
162
+ console.log(`${pc.dim('-')} ${info.label} ${pc.dim('(not signed in)')}`)
163
+ console.log(pc.dim(` run: egaki login → select ChatGPT`))
164
+ }
165
+ continue
166
+ }
167
+
124
168
  const icon = status.available ? pc.green('*') : pc.dim('-')
125
169
  const source = (() => {
126
170
  if (status.source === 'env') {
@@ -150,7 +194,8 @@ export function removeLogin(provider: string): void {
150
194
  }
151
195
 
152
196
  removeProviderKey(provider)
153
- console.log(pc.green(`${info.label} key removed`))
197
+ const label = provider === 'chatgpt' ? 'ChatGPT OAuth' : info.label
198
+ console.log(pc.green(`${label} key removed`))
154
199
  }
155
200
 
156
201
  // Read API key from stdin (for piping: echo "sk-xxx" | egaki login --provider google)
@@ -109,6 +109,22 @@ const xaiImage = {
109
109
  strategy: 'image' as const,
110
110
  }
111
111
 
112
+ const vertexImagen = {
113
+ provider: 'vertex',
114
+ strategy: 'image' as const,
115
+ features: {
116
+ ...googleImagen.features,
117
+ },
118
+ }
119
+
120
+ const vertexText = {
121
+ provider: 'vertex',
122
+ strategy: 'text' as const,
123
+ features: {
124
+ ...googleText.features,
125
+ },
126
+ }
127
+
112
128
  const fluxAspectRatios = ['1:1', '3:4', '4:3', '9:16', '16:9', '9:21', '21:9']
113
129
 
114
130
  // ─── catalog ─────────────────────────────────────────────────────────────────
@@ -196,6 +212,64 @@ export const CATALOG: ModelEntry[] = [
196
212
  cost: { type: 'per-token', inputPerM: 0.5, outputPerM: 3.0 },
197
213
  },
198
214
 
215
+ // ── Vertex: Imagen ──────────────────────────────────────────────────────
216
+ // Same models as Google AI Studio but routed through Vertex AI / Google Cloud billing.
217
+ // Use `egaki login --provider vertex --key <key>` to configure.
218
+ {
219
+ id: 'vertex/imagen-4.0-generate-001',
220
+ name: 'Imagen 4 (Vertex)',
221
+ released: '2025-08-15',
222
+ ...vertexImagen,
223
+ cost: { type: 'per-image', perImage: 0.04 },
224
+ },
225
+ {
226
+ id: 'vertex/imagen-4.0-ultra-generate-001',
227
+ name: 'Imagen 4 Ultra (Vertex)',
228
+ released: '2025-08-15',
229
+ ...vertexImagen,
230
+ cost: { type: 'per-image', perImage: 0.06 },
231
+ },
232
+ {
233
+ id: 'vertex/imagen-4.0-fast-generate-001',
234
+ name: 'Imagen 4 Fast (Vertex)',
235
+ released: '2025-08-15',
236
+ ...vertexImagen,
237
+ cost: { type: 'per-image', perImage: 0.02 },
238
+ },
239
+
240
+ // ── Vertex: Gemini text+image ─────────────────────────────────────────
241
+ {
242
+ id: 'vertex/gemini-2.5-flash-image',
243
+ name: 'Gemini 2.5 Flash Image (Vertex)',
244
+ released: '2025-08-26',
245
+ ...vertexText,
246
+ cost: { type: 'per-token', inputPerM: 0.3, outputPerM: 30 },
247
+ },
248
+ {
249
+ id: 'vertex/gemini-3-pro-image-preview',
250
+ name: 'Gemini 3 Pro Image (Vertex)',
251
+ released: '2025-11-20',
252
+ ...vertexText,
253
+ cost: { type: 'per-token', inputPerM: 1.25, outputPerM: 10 },
254
+ },
255
+ {
256
+ id: 'vertex/gemini-3.1-flash-image-preview',
257
+ name: 'Gemini 3.1 Flash Image (Vertex)',
258
+ released: '2026-02-26',
259
+ ...vertexText,
260
+ features: {
261
+ ...vertexText.features,
262
+ aspectRatios: [
263
+ ...vertexText.features.aspectRatios,
264
+ '4:1',
265
+ '1:4',
266
+ '8:1',
267
+ '1:8',
268
+ ],
269
+ },
270
+ cost: { type: 'per-token', inputPerM: 0.5, outputPerM: 3.0 },
271
+ },
272
+
199
273
  // ── BFL (AI Gateway) ───────────────────────────────────────────────────
200
274
  {
201
275
  id: 'flux-kontext-max',
@@ -432,9 +506,24 @@ export const CATALOG: ModelEntry[] = [
432
506
  multipleImages: true,
433
507
  },
434
508
  },
435
- // NOTE: chatgpt-image-latest exists in OpenAI's .d.ts types but it's a moving
436
- // alias used by ChatGPT/Responses API, not a stable image endpoint model.
437
- // Omitted until OpenAI stabilizes it as a first-class image generation target.
509
+ {
510
+ id: 'chatgpt-image-latest',
511
+ name: 'ChatGPT Image',
512
+ description:
513
+ 'Rolling-latest alias that tracks the newest ChatGPT image model. Currently points ' +
514
+ 'to gpt-image-1.5. Pricing may shift when the alias retargets to a newer model.',
515
+ released: '2026-01',
516
+ ...openaiImage,
517
+ cost: { type: 'per-image', perImage: 0.034 },
518
+ features: {
519
+ editing: true,
520
+ inpainting: true,
521
+ aspectRatios: [],
522
+ sizes: ['1024x1024', '1536x1024', '1024x1536'],
523
+ seed: false,
524
+ multipleImages: true,
525
+ },
526
+ },
438
527
 
439
528
  // ── Replicate ──────────────────────────────────────────────────────────
440
529
  {
package/src/models.ts CHANGED
@@ -17,7 +17,7 @@
17
17
  // dist/index.d.ts and add entries here.
18
18
  import type { ImageModel, LanguageModel } from 'ai'
19
19
  import pc from 'picocolors'
20
- import { PROVIDERS, EGAKI_GATEWAY_URL } from './credentials.js'
20
+ import { PROVIDERS, EGAKI_GATEWAY_URL, shouldUseChatGptBackend } from './credentials.js'
21
21
  import { CATALOG, findModel } from './model-catalog.js'
22
22
  import type { ModelEntry } from './model-catalog.js'
23
23
  import { VIDEO_CATALOG, findVideoModel } from './video-model-catalog.js'
@@ -29,6 +29,16 @@ export type { ModelEntry, VideoModelEntry }
29
29
  export const IMAGE_MODELS = CATALOG.map((m) => m.id) as [string, ...string[]]
30
30
  export const VIDEO_MODELS = VIDEO_CATALOG.map((m) => m.id) as [string, ...string[]]
31
31
 
32
+ /**
33
+ * Strip provider prefix from a model ID (e.g. "vertex/imagen-4.0-generate-001" → "imagen-4.0-generate-001").
34
+ * Provider SDKs expect bare model IDs, but the catalog uses prefixed IDs for routing.
35
+ */
36
+ function stripProviderPrefix(modelId: string): string {
37
+ const slash = modelId.indexOf('/')
38
+ if (slash === -1) return modelId
39
+ return modelId.slice(slash + 1)
40
+ }
41
+
32
42
  export const DEFAULT_MODEL = 'nano-banana-pro-preview'
33
43
  export const DEFAULT_VIDEO_MODEL = 'veo-3.1-fast-generate-001'
34
44
 
@@ -66,6 +76,20 @@ export function ensureProviderKey(providerName: string): void {
66
76
  // If direct provider key exists, we're good
67
77
  if (hasDirectProviderKey(providerName)) return
68
78
 
79
+ // Vertex models require a direct key — the upstream Vercel AI Gateway
80
+ // does not support vertex/ routing, so egaki gateway can't proxy them.
81
+ if (providerName === 'vertex') {
82
+ console.error('')
83
+ console.error(pc.red(pc.bold('Vertex models require a direct Google Cloud API key')))
84
+ console.error('')
85
+ console.error(` ${pc.cyan('egaki login --provider vertex --key <key>')}`)
86
+ console.error('')
87
+ console.error(pc.dim('Get a key at https://console.cloud.google.com/apis/credentials'))
88
+ console.error(pc.dim('Egaki subscription does not cover Vertex routing yet.'))
89
+ console.error('')
90
+ process.exit(1)
91
+ }
92
+
69
93
  // If egaki key exists, we'll route through the gateway
70
94
  if (hasEgakiKey()) return
71
95
 
@@ -109,8 +133,11 @@ async function createGatewayImageModel(modelId: string, provider: string): Promi
109
133
  apiKey: process.env['EGAKI_API_KEY']!,
110
134
  baseURL: EGAKI_GATEWAY_URL,
111
135
  })
112
- // Gateway expects provider/model format for routing
113
- const gatewayModelId = `${provider}/${modelId}`
136
+ // Gateway expects provider/model format for routing.
137
+ // For models that already have a prefix (e.g. vertex/imagen-4.0), strip it
138
+ // and use the catalog provider. For bare IDs, prepend the provider.
139
+ const bareId = stripProviderPrefix(modelId)
140
+ const gatewayModelId = `${provider}/${bareId}`
114
141
  return gateway.image(gatewayModelId)
115
142
  }
116
143
 
@@ -123,7 +150,8 @@ async function createGatewayTextModel(modelId: string, provider: string): Promis
123
150
  apiKey: process.env['EGAKI_API_KEY']!,
124
151
  baseURL: EGAKI_GATEWAY_URL,
125
152
  })
126
- const gatewayModelId = `${provider}/${modelId}`
153
+ const bareId = stripProviderPrefix(modelId)
154
+ const gatewayModelId = `${provider}/${bareId}`
127
155
  return gateway(gatewayModelId)
128
156
  }
129
157
 
@@ -136,10 +164,21 @@ async function createGatewayVideoModel(modelId: string, provider: string) {
136
164
  apiKey: process.env['EGAKI_API_KEY']!,
137
165
  baseURL: EGAKI_GATEWAY_URL,
138
166
  })
139
- const gatewayModelId = `${provider}/${modelId}`
167
+ const bareId = stripProviderPrefix(modelId)
168
+ const gatewayModelId = `${provider}/${bareId}`
140
169
  return gateway.video(gatewayModelId)
141
170
  }
142
171
 
172
+ /**
173
+ * ChatGPT OAuth image generation goes through the Codex backend instead of the
174
+ * OpenAI Image API, so OpenAI image models need the custom responses path.
175
+ */
176
+ export function shouldUseResponsesApi(modelId: string): boolean {
177
+ const config = findModel(modelId)
178
+ if (!config) return false
179
+ return config.provider === 'openai' && config.strategy === 'image' && shouldUseChatGptBackend()
180
+ }
181
+
143
182
  // Lazily import the provider and create the right model instance.
144
183
  // This avoids loading all provider SDKs upfront — only the one needed
145
184
  // for the selected model gets imported.
@@ -156,6 +195,10 @@ export async function createImageModel(modelId: string): Promise<ImageModel> {
156
195
  const { google } = await import('@ai-sdk/google')
157
196
  return google.image(modelId)
158
197
  }
198
+ case 'vertex': {
199
+ const { vertex } = await import('@ai-sdk/google-vertex')
200
+ return vertex.image(stripProviderPrefix(modelId))
201
+ }
159
202
  case 'openai': {
160
203
  const { openai } = await import('@ai-sdk/openai')
161
204
  return openai.image(modelId)
@@ -198,11 +241,15 @@ export async function createTextModel(
198
241
  const { google } = await import('@ai-sdk/google')
199
242
  return google(modelId)
200
243
  }
244
+ case 'vertex': {
245
+ const { vertex } = await import('@ai-sdk/google-vertex')
246
+ return vertex(stripProviderPrefix(modelId))
247
+ }
201
248
  default:
202
- // Only Google supports generateText with responseModalities for images
249
+ // Only Google/Vertex support generateText with responseModalities for images
203
250
  console.error(
204
251
  pc.red(
205
- `Text+image generation is only supported for Google models, got provider: ${config.provider}`,
252
+ `Text+image generation is only supported for Google/Vertex models, got provider: ${config.provider}`,
206
253
  ),
207
254
  )
208
255
  process.exit(1)
@@ -234,6 +281,10 @@ export async function createVideoModel(modelId: string): Promise<any> {
234
281
  const { google } = await import('@ai-sdk/google')
235
282
  return google.video(modelId)
236
283
  }
284
+ case 'vertex': {
285
+ const { vertex } = await import('@ai-sdk/google-vertex')
286
+ return vertex.video(stripProviderPrefix(modelId))
287
+ }
237
288
  case 'fal': {
238
289
  const { fal } = await import('@ai-sdk/fal')
239
290
  return fal.video(modelId)
@@ -241,7 +292,7 @@ export async function createVideoModel(modelId: string): Promise<any> {
241
292
  default:
242
293
  console.error(
243
294
  pc.red(
244
- `Direct video generation is only supported for Google and Fal keys, got provider: ${config.provider}`,
295
+ `Direct video generation is only supported for Google, Vertex, and Fal keys, got provider: ${config.provider}`,
245
296
  ),
246
297
  )
247
298
  process.exit(1)
@@ -169,6 +169,67 @@ export const VIDEO_CATALOG: VideoModelEntry[] = [
169
169
  multipleVideos: true,
170
170
  },
171
171
  },
172
+ // ── Vertex: Veo ──────────────────────────────────────────────────────
173
+ // Same Veo models routed through Vertex AI / Google Cloud billing.
174
+ // Use `egaki login --provider vertex --key <key>` to configure.
175
+ {
176
+ id: 'vertex/veo-3.1-generate-001',
177
+ name: 'Veo 3.1 (Vertex)',
178
+ provider: 'vertex',
179
+ strategy: 'video',
180
+ released: '2026-01',
181
+ cost: {
182
+ type: 'per-video-second',
183
+ defaultDurationSec: 8,
184
+ tiers: [
185
+ { resolution: '720p', audio: false, costPerSecond: 0.2 },
186
+ { resolution: '720p', audio: true, costPerSecond: 0.4 },
187
+ { resolution: '1080p', audio: false, costPerSecond: 0.2 },
188
+ { resolution: '1080p', audio: true, costPerSecond: 0.4 },
189
+ { resolution: '4k', audio: false, costPerSecond: 0.4 },
190
+ { resolution: '4k', audio: true, costPerSecond: 0.6 },
191
+ ],
192
+ },
193
+ features: {
194
+ textToVideo: true,
195
+ imageToVideo: false,
196
+ capabilities: ['t2v'],
197
+ aspectRatios: ['16:9', '9:16'],
198
+ resolutions: ['720p', '1080p', '4k'],
199
+ durationRangeSec: { min: 4, max: 8 },
200
+ seed: true,
201
+ multipleVideos: true,
202
+ },
203
+ },
204
+ {
205
+ id: 'vertex/veo-3.1-fast-generate-001',
206
+ name: 'Veo 3.1 Fast (Vertex)',
207
+ provider: 'vertex',
208
+ strategy: 'video',
209
+ released: '2026-01',
210
+ cost: {
211
+ type: 'per-video-second',
212
+ defaultDurationSec: 8,
213
+ tiers: [
214
+ { resolution: '720p', audio: false, costPerSecond: 0.1 },
215
+ { resolution: '720p', audio: true, costPerSecond: 0.15 },
216
+ { resolution: '1080p', audio: false, costPerSecond: 0.1 },
217
+ { resolution: '1080p', audio: true, costPerSecond: 0.15 },
218
+ { resolution: '4k', audio: false, costPerSecond: 0.3 },
219
+ { resolution: '4k', audio: true, costPerSecond: 0.35 },
220
+ ],
221
+ },
222
+ features: {
223
+ textToVideo: true,
224
+ imageToVideo: false,
225
+ capabilities: ['t2v'],
226
+ aspectRatios: ['16:9', '9:16'],
227
+ resolutions: ['720p', '1080p', '4k'],
228
+ durationRangeSec: { min: 4, max: 8 },
229
+ seed: true,
230
+ multipleVideos: true,
231
+ },
232
+ },
172
233
  {
173
234
  id: 'grok-imagine-video',
174
235
  name: 'Grok Imagine Video',