egaki 0.0.2 → 0.1.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/models.ts CHANGED
@@ -20,15 +20,20 @@ import pc from 'picocolors'
20
20
  import { PROVIDERS, EGAKI_GATEWAY_URL } from './credentials.js'
21
21
  import { CATALOG, findModel } from './model-catalog.js'
22
22
  import type { ModelEntry } from './model-catalog.js'
23
+ import { VIDEO_CATALOG, findVideoModel } from './video-model-catalog.js'
24
+ import type { VideoModelEntry } from './video-model-catalog.js'
23
25
 
24
- export type { ModelEntry }
26
+ export type AnyModelEntry = ModelEntry | VideoModelEntry
27
+ export type { ModelEntry, VideoModelEntry }
25
28
 
26
29
  export const IMAGE_MODELS = CATALOG.map((m) => m.id) as [string, ...string[]]
30
+ export const VIDEO_MODELS = VIDEO_CATALOG.map((m) => m.id) as [string, ...string[]]
27
31
 
28
32
  export const DEFAULT_MODEL = 'nano-banana-pro-preview'
33
+ export const DEFAULT_VIDEO_MODEL = 'veo-3.1-fast-generate-001'
29
34
 
30
- export function getModelConfig(modelId: string): ModelEntry {
31
- const entry = findModel(modelId)
35
+ export function getModelConfig(modelId: string): AnyModelEntry {
36
+ const entry = findModel(modelId) ?? findVideoModel(modelId)
32
37
  if (!entry) {
33
38
  console.error(pc.red(`Unknown model: ${modelId}`))
34
39
  process.exit(1)
@@ -122,6 +127,19 @@ async function createGatewayTextModel(modelId: string, provider: string): Promis
122
127
  return gateway(gatewayModelId)
123
128
  }
124
129
 
130
+ /**
131
+ * Create a gateway-backed video model using createGateway from the AI SDK.
132
+ */
133
+ async function createGatewayVideoModel(modelId: string, provider: string) {
134
+ const { createGateway } = await import('ai')
135
+ const gateway = createGateway({
136
+ apiKey: process.env['EGAKI_API_KEY']!,
137
+ baseURL: EGAKI_GATEWAY_URL,
138
+ })
139
+ const gatewayModelId = `${provider}/${modelId}`
140
+ return gateway.video(gatewayModelId)
141
+ }
142
+
125
143
  // Lazily import the provider and create the right model instance.
126
144
  // This avoids loading all provider SDKs upfront — only the one needed
127
145
  // for the selected model gets imported.
@@ -167,6 +185,10 @@ export async function createTextModel(
167
185
  modelId: string,
168
186
  ): Promise<LanguageModel> {
169
187
  const config = getModelConfig(modelId)
188
+ if (config.strategy !== 'text') {
189
+ console.error(pc.red(`Model ${modelId} is not a text model`))
190
+ process.exit(1)
191
+ }
170
192
  ensureProviderKey(config.provider)
171
193
 
172
194
  // Direct provider key takes priority
@@ -195,3 +217,42 @@ export async function createTextModel(
195
217
  console.error(pc.red(`No API key available for provider: ${config.provider}`))
196
218
  process.exit(1)
197
219
  }
220
+
221
+ export async function createVideoModel(modelId: string): Promise<any> {
222
+ const config = getModelConfig(modelId)
223
+ if (config.strategy !== 'video') {
224
+ console.error(pc.red(`Model ${modelId} is not a video model`))
225
+ process.exit(1)
226
+ }
227
+
228
+ ensureProviderKey(config.provider)
229
+
230
+ // Direct provider key takes priority
231
+ if (hasDirectProviderKey(config.provider)) {
232
+ switch (config.provider) {
233
+ case 'google': {
234
+ const { google } = await import('@ai-sdk/google')
235
+ return google.video(modelId)
236
+ }
237
+ case 'fal': {
238
+ const { fal } = await import('@ai-sdk/fal')
239
+ return fal.video(modelId)
240
+ }
241
+ default:
242
+ console.error(
243
+ pc.red(
244
+ `Direct video generation is only supported for Google and Fal keys, got provider: ${config.provider}`,
245
+ ),
246
+ )
247
+ process.exit(1)
248
+ }
249
+ }
250
+
251
+ // Fall back to egaki gateway
252
+ if (hasEgakiKey()) {
253
+ return createGatewayVideoModel(modelId, config.provider)
254
+ }
255
+
256
+ console.error(pc.red(`No API key available for provider: ${config.provider}`))
257
+ process.exit(1)
258
+ }
@@ -0,0 +1,48 @@
1
+ // Cross-platform browser opener for URLs used by CLI flows.
2
+ // Never throws: missing OS commands are handled gracefully.
3
+ import { spawnSync } from 'node:child_process'
4
+
5
+ type OpenCommand = {
6
+ command: string
7
+ args: string[]
8
+ }
9
+
10
+ function getOpenCommands(url: string): OpenCommand[] {
11
+ if (process.platform === 'darwin') {
12
+ return [{ command: 'open', args: [url] }]
13
+ }
14
+
15
+ if (process.platform === 'win32') {
16
+ // `start` is a cmd builtin, so invoke through cmd.exe.
17
+ return [{ command: 'cmd', args: ['/c', 'start', '', url] }]
18
+ }
19
+
20
+ return [
21
+ { command: 'xdg-open', args: [url] },
22
+ { command: 'gio', args: ['open', url] },
23
+ ]
24
+ }
25
+
26
+ export function openUrlInBrowser(url: string): boolean {
27
+ // Avoid launching browser commands in non-interactive contexts (CI/pipes).
28
+ if (!process.stdout.isTTY || !process.stdin.isTTY) {
29
+ return false
30
+ }
31
+
32
+ for (const candidate of getOpenCommands(url)) {
33
+ try {
34
+ const result = spawnSync(candidate.command, candidate.args, {
35
+ stdio: 'ignore',
36
+ windowsHide: true,
37
+ })
38
+
39
+ if (!result.error && result.status === 0) {
40
+ return true
41
+ }
42
+ } catch {
43
+ // Keep trying fallback commands. This helper must never crash the CLI.
44
+ }
45
+ }
46
+
47
+ return false
48
+ }
@@ -16,9 +16,10 @@ import {
16
16
  spinner,
17
17
  } from '@clack/prompts'
18
18
  import { EGAKI_GATEWAY_URL, saveProviderKey, getProviderKey } from './credentials.js'
19
+ import { openUrlInBrowser } from './open-browser.js'
19
20
 
20
- // Strip /v1/ai suffix to get the base URL for non-proxy endpoints (/buy, /api/*)
21
- const GATEWAY_BASE = EGAKI_GATEWAY_URL.replace(/\/v1\/ai$/, '')
21
+ // Strip /vN/ai suffix to get the base URL for non-proxy endpoints (/buy, /api/*)
22
+ const GATEWAY_BASE = EGAKI_GATEWAY_URL.replace(/\/v\d+\/ai$/, '')
22
23
 
23
24
  type PlanInfo = {
24
25
  id: string
@@ -86,24 +87,42 @@ export async function subscribeInteractive(): Promise<void> {
86
87
  process.exit(0)
87
88
  }
88
89
 
89
- // Get email
90
- const email = await text({
91
- message: 'Email for the subscription',
92
- placeholder: 'you@example.com',
93
- validate: (value) => {
94
- if (!value || !value.includes('@')) {
95
- return 'Please enter a valid email'
96
- }
97
- },
90
+ // Optional email prefill for checkout
91
+ const prefillEmail = await confirm({
92
+ message: 'Prefill your email in checkout URL? (optional)',
98
93
  })
99
94
 
100
- if (isCancel(email)) {
95
+ if (isCancel(prefillEmail)) {
101
96
  cancel('Subscribe cancelled.')
102
97
  process.exit(0)
103
98
  }
104
99
 
100
+ let email: string | undefined
101
+ if (prefillEmail) {
102
+ const enteredEmail = await text({
103
+ message: 'Email for the subscription',
104
+ placeholder: 'you@example.com',
105
+ validate: (value) => {
106
+ if (!value || !value.includes('@')) {
107
+ return 'Please enter a valid email'
108
+ }
109
+ },
110
+ })
111
+
112
+ if (isCancel(enteredEmail)) {
113
+ cancel('Subscribe cancelled.')
114
+ process.exit(0)
115
+ }
116
+
117
+ email = enteredEmail
118
+ }
119
+
105
120
  // Show checkout URL
106
- const checkoutUrl = `${GATEWAY_BASE}/buy?plan=${planId}&email=${encodeURIComponent(email)}`
121
+ const params = new URLSearchParams({ plan: planId })
122
+ if (email) {
123
+ params.set('email', email)
124
+ }
125
+ const checkoutUrl = `${GATEWAY_BASE}/buy?${params.toString()}`
107
126
 
108
127
  log.info('')
109
128
  note(
@@ -120,6 +139,13 @@ export async function subscribeInteractive(): Promise<void> {
120
139
  'Checkout',
121
140
  )
122
141
 
142
+ const opened = openUrlInBrowser(checkoutUrl)
143
+ if (opened) {
144
+ log.success('Opened checkout URL in your browser')
145
+ } else if (process.stdout.isTTY && process.stdin.isTTY) {
146
+ log.info('Could not auto-open a browser. Open the URL above manually.')
147
+ }
148
+
123
149
  // Ask if they want to enter the key now
124
150
  const hasKey = await confirm({
125
151
  message: 'Do you already have your API key?',
@@ -176,7 +202,8 @@ export function subscribeNonInteractive(email?: string, plan?: string): void {
176
202
  }
177
203
 
178
204
  console.log('')
179
- console.log('After payment, you\'ll receive your API key via email.')
205
+ console.log('After payment, your API key is shown on the success page.')
206
+ console.log('If an email was provided to Stripe checkout, a copy is also sent by email.')
180
207
  console.log(`Then run: ${pc.cyan('egaki login --provider egaki --key egaki_...')}`)
181
208
  console.log('')
182
209
  console.log('Tip: egaki supports both BYOK and subscription mode.')
@@ -0,0 +1,302 @@
1
+ // Single source of truth for video generation models.
2
+ // Stores provider, strategy, capabilities, and duration-based pricing metadata.
3
+ // Gateway billing uses this catalog to estimate per-request video cost.
4
+
5
+ export type VideoDurationPricingTier = {
6
+ /** USD per second for this variant */
7
+ costPerSecond: number
8
+ /** Optional resolution discriminator (e.g. 720p, 1080p, 4k) */
9
+ resolution?: string
10
+ /** Optional mode discriminator (e.g. std, pro) */
11
+ mode?: string
12
+ /** Optional audio discriminator */
13
+ audio?: boolean
14
+ }
15
+
16
+ export type PerVideoSecondCost = {
17
+ type: 'per-video-second'
18
+ /** Fallback duration when request does not specify one (seconds) */
19
+ defaultDurationSec: number
20
+ tiers: VideoDurationPricingTier[]
21
+ }
22
+
23
+ export type UnknownVideoCost = {
24
+ type: 'unknown'
25
+ }
26
+
27
+ export type VideoModelCost = PerVideoSecondCost | UnknownVideoCost
28
+
29
+ export type VideoModelFeatures = {
30
+ /** Supports text prompt to video generation */
31
+ textToVideo: boolean
32
+ /** Supports image-to-video prompt object ({ image, text }) */
33
+ imageToVideo: boolean
34
+ /** Optional capabilities exposed by provider */
35
+ capabilities: Array<'t2v' | 'i2v' | 'r2v' | 'motion-control' | 'editing'>
36
+ /** Optional supported aspect ratios */
37
+ aspectRatios?: string[]
38
+ /** Optional supported resolutions */
39
+ resolutions?: string[]
40
+ /** Optional duration range in seconds */
41
+ durationRangeSec?: { min: number; max: number }
42
+ seed: boolean
43
+ multipleVideos: boolean
44
+ }
45
+
46
+ export type VideoModelEntry = {
47
+ id: string
48
+ name: string
49
+ description?: string
50
+ provider: string
51
+ strategy: 'video'
52
+ released: string
53
+ cost: VideoModelCost
54
+ features: VideoModelFeatures
55
+ }
56
+
57
+ const commonRatios = ['16:9', '9:16', '1:1', '4:3', '3:4']
58
+
59
+ export const VIDEO_CATALOG: VideoModelEntry[] = [
60
+ {
61
+ id: 'veo-3.1-generate-001',
62
+ name: 'Veo 3.1',
63
+ provider: 'google',
64
+ strategy: 'video',
65
+ released: '2026-01',
66
+ cost: {
67
+ type: 'per-video-second',
68
+ defaultDurationSec: 8,
69
+ tiers: [
70
+ { resolution: '720p', audio: false, costPerSecond: 0.2 },
71
+ { resolution: '720p', audio: true, costPerSecond: 0.4 },
72
+ { resolution: '1080p', audio: false, costPerSecond: 0.2 },
73
+ { resolution: '1080p', audio: true, costPerSecond: 0.4 },
74
+ { resolution: '4k', audio: false, costPerSecond: 0.4 },
75
+ { resolution: '4k', audio: true, costPerSecond: 0.6 },
76
+ ],
77
+ },
78
+ features: {
79
+ textToVideo: true,
80
+ imageToVideo: false,
81
+ capabilities: ['t2v'],
82
+ aspectRatios: ['16:9', '9:16'],
83
+ resolutions: ['720p', '1080p', '4k'],
84
+ durationRangeSec: { min: 4, max: 8 },
85
+ seed: true,
86
+ multipleVideos: true,
87
+ },
88
+ },
89
+ {
90
+ id: 'veo-3.1-fast-generate-001',
91
+ name: 'Veo 3.1 Fast',
92
+ provider: 'google',
93
+ strategy: 'video',
94
+ released: '2026-01',
95
+ cost: {
96
+ type: 'per-video-second',
97
+ defaultDurationSec: 8,
98
+ tiers: [
99
+ { resolution: '720p', audio: false, costPerSecond: 0.1 },
100
+ { resolution: '720p', audio: true, costPerSecond: 0.15 },
101
+ { resolution: '1080p', audio: false, costPerSecond: 0.1 },
102
+ { resolution: '1080p', audio: true, costPerSecond: 0.15 },
103
+ { resolution: '4k', audio: false, costPerSecond: 0.3 },
104
+ { resolution: '4k', audio: true, costPerSecond: 0.35 },
105
+ ],
106
+ },
107
+ features: {
108
+ textToVideo: true,
109
+ imageToVideo: false,
110
+ capabilities: ['t2v'],
111
+ aspectRatios: ['16:9', '9:16'],
112
+ resolutions: ['720p', '1080p', '4k'],
113
+ durationRangeSec: { min: 4, max: 8 },
114
+ seed: true,
115
+ multipleVideos: true,
116
+ },
117
+ },
118
+ {
119
+ id: 'veo-3.0-generate-001',
120
+ name: 'Veo 3.0',
121
+ provider: 'google',
122
+ strategy: 'video',
123
+ released: '2025-12',
124
+ cost: {
125
+ type: 'per-video-second',
126
+ defaultDurationSec: 8,
127
+ tiers: [
128
+ { resolution: '720p', audio: false, costPerSecond: 0.2 },
129
+ { resolution: '720p', audio: true, costPerSecond: 0.4 },
130
+ { resolution: '1080p', audio: false, costPerSecond: 0.2 },
131
+ { resolution: '1080p', audio: true, costPerSecond: 0.4 },
132
+ ],
133
+ },
134
+ features: {
135
+ textToVideo: true,
136
+ imageToVideo: false,
137
+ capabilities: ['t2v'],
138
+ aspectRatios: ['16:9', '9:16'],
139
+ resolutions: ['720p', '1080p'],
140
+ durationRangeSec: { min: 4, max: 8 },
141
+ seed: true,
142
+ multipleVideos: true,
143
+ },
144
+ },
145
+ {
146
+ id: 'veo-3.0-fast-generate-001',
147
+ name: 'Veo 3.0 Fast',
148
+ provider: 'google',
149
+ strategy: 'video',
150
+ released: '2025-12',
151
+ cost: {
152
+ type: 'per-video-second',
153
+ defaultDurationSec: 8,
154
+ tiers: [
155
+ { resolution: '720p', audio: false, costPerSecond: 0.1 },
156
+ { resolution: '720p', audio: true, costPerSecond: 0.15 },
157
+ { resolution: '1080p', audio: false, costPerSecond: 0.1 },
158
+ { resolution: '1080p', audio: true, costPerSecond: 0.15 },
159
+ ],
160
+ },
161
+ features: {
162
+ textToVideo: true,
163
+ imageToVideo: false,
164
+ capabilities: ['t2v'],
165
+ aspectRatios: ['16:9', '9:16'],
166
+ resolutions: ['720p', '1080p'],
167
+ durationRangeSec: { min: 4, max: 8 },
168
+ seed: true,
169
+ multipleVideos: true,
170
+ },
171
+ },
172
+ {
173
+ id: 'grok-imagine-video',
174
+ name: 'Grok Imagine Video',
175
+ provider: 'xai',
176
+ strategy: 'video',
177
+ released: '2026-03',
178
+ cost: {
179
+ type: 'per-video-second',
180
+ defaultDurationSec: 5,
181
+ tiers: [
182
+ { resolution: '480p', costPerSecond: 0.05 },
183
+ { resolution: '720p', costPerSecond: 0.07 },
184
+ ],
185
+ },
186
+ features: {
187
+ textToVideo: true,
188
+ imageToVideo: true,
189
+ capabilities: ['t2v', 'i2v', 'editing'],
190
+ aspectRatios: ['1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3'],
191
+ resolutions: ['480p', '720p'],
192
+ durationRangeSec: { min: 1, max: 15 },
193
+ seed: false,
194
+ multipleVideos: false,
195
+ },
196
+ },
197
+ {
198
+ id: 'kling-v2.6-t2v',
199
+ name: 'Kling v2.6 T2V',
200
+ provider: 'klingai',
201
+ strategy: 'video',
202
+ released: '2025-12',
203
+ cost: {
204
+ type: 'per-video-second',
205
+ defaultDurationSec: 5,
206
+ tiers: [
207
+ { mode: 'std', costPerSecond: 0.042 },
208
+ { mode: 'pro', audio: false, costPerSecond: 0.07 },
209
+ { mode: 'pro', audio: true, costPerSecond: 0.14 },
210
+ ],
211
+ },
212
+ features: {
213
+ textToVideo: true,
214
+ imageToVideo: false,
215
+ capabilities: ['t2v'],
216
+ aspectRatios: commonRatios,
217
+ durationRangeSec: { min: 5, max: 10 },
218
+ seed: false,
219
+ multipleVideos: false,
220
+ },
221
+ },
222
+ {
223
+ id: 'wan-v2.6-t2v',
224
+ name: 'Wan v2.6 T2V',
225
+ provider: 'alibaba',
226
+ strategy: 'video',
227
+ released: '2026-01',
228
+ cost: {
229
+ type: 'per-video-second',
230
+ defaultDurationSec: 5,
231
+ tiers: [
232
+ { resolution: '720p', costPerSecond: 0.1 },
233
+ { resolution: '1080p', costPerSecond: 0.15 },
234
+ ],
235
+ },
236
+ features: {
237
+ textToVideo: true,
238
+ imageToVideo: false,
239
+ capabilities: ['t2v'],
240
+ resolutions: ['720p', '1080p'],
241
+ durationRangeSec: { min: 2, max: 15 },
242
+ seed: false,
243
+ multipleVideos: false,
244
+ },
245
+ },
246
+
247
+ // Fal direct BYOK models — duration pricing depends on Fal endpoint/config.
248
+ {
249
+ id: 'luma-ray-2',
250
+ name: 'Luma Ray 2 (Fal)',
251
+ provider: 'fal',
252
+ strategy: 'video',
253
+ released: '2025-11',
254
+ cost: { type: 'unknown' },
255
+ features: {
256
+ textToVideo: true,
257
+ imageToVideo: true,
258
+ capabilities: ['t2v', 'i2v'],
259
+ aspectRatios: commonRatios,
260
+ seed: true,
261
+ multipleVideos: false,
262
+ },
263
+ },
264
+ {
265
+ id: 'minimax-video',
266
+ name: 'MiniMax Video (Fal)',
267
+ provider: 'fal',
268
+ strategy: 'video',
269
+ released: '2025-10',
270
+ cost: { type: 'unknown' },
271
+ features: {
272
+ textToVideo: true,
273
+ imageToVideo: false,
274
+ capabilities: ['t2v'],
275
+ aspectRatios: commonRatios,
276
+ seed: false,
277
+ multipleVideos: false,
278
+ },
279
+ },
280
+ {
281
+ id: 'hunyuan-video',
282
+ name: 'Hunyuan Video (Fal)',
283
+ provider: 'fal',
284
+ strategy: 'video',
285
+ released: '2025-09',
286
+ cost: { type: 'unknown' },
287
+ features: {
288
+ textToVideo: true,
289
+ imageToVideo: true,
290
+ capabilities: ['t2v', 'i2v'],
291
+ aspectRatios: commonRatios,
292
+ seed: true,
293
+ multipleVideos: false,
294
+ },
295
+ },
296
+ ]
297
+
298
+ const videoCatalogIndex = new Map(VIDEO_CATALOG.map((m) => [m.id, m]))
299
+
300
+ export function findVideoModel(id: string): VideoModelEntry | undefined {
301
+ return videoCatalogIndex.get(id)
302
+ }