egaki 0.6.0 → 0.7.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 (271) hide show
  1. package/CHANGELOG.md +140 -9
  2. package/dist/cli/cache-utils.d.ts +33 -0
  3. package/dist/cli/cache-utils.d.ts.map +1 -0
  4. package/dist/cli/cache-utils.js +157 -0
  5. package/dist/cli/cached-generate.d.ts +105 -0
  6. package/dist/cli/cached-generate.d.ts.map +1 -0
  7. package/dist/cli/cached-generate.js +208 -0
  8. package/dist/cli/cached-generate.test.d.ts +11 -0
  9. package/dist/cli/cached-generate.test.d.ts.map +1 -0
  10. package/dist/cli/cached-generate.test.js +431 -0
  11. package/dist/cli/cartesia-provider.d.ts +16 -0
  12. package/dist/cli/cartesia-provider.d.ts.map +1 -0
  13. package/dist/cli/cartesia-provider.js +184 -0
  14. package/dist/cli/chatgpt-auth.d.ts.map +1 -0
  15. package/dist/cli/cli.d.ts +4 -0
  16. package/dist/cli/cli.d.ts.map +1 -0
  17. package/dist/{cli.js → cli/cli.js} +626 -600
  18. package/dist/cli/clone-generate.d.ts +26 -0
  19. package/dist/cli/clone-generate.d.ts.map +1 -0
  20. package/dist/cli/clone-generate.js +148 -0
  21. package/dist/cli/credentials.d.ts.map +1 -0
  22. package/dist/{credentials.js → cli/credentials.js} +20 -0
  23. package/dist/cli/elevenlabs-speech-provider.d.ts +26 -0
  24. package/dist/cli/elevenlabs-speech-provider.d.ts.map +1 -0
  25. package/dist/cli/elevenlabs-speech-provider.js +199 -0
  26. package/dist/cli/generate.d.ts +140 -0
  27. package/dist/cli/generate.d.ts.map +1 -0
  28. package/dist/cli/generate.js +604 -0
  29. package/dist/cli/generate.test.d.ts +2 -0
  30. package/dist/cli/generate.test.d.ts.map +1 -0
  31. package/dist/cli/generate.test.js +131 -0
  32. package/dist/cli/login.d.ts.map +1 -0
  33. package/dist/cli/main.d.ts +3 -0
  34. package/dist/cli/main.d.ts.map +1 -0
  35. package/dist/cli/main.js +7 -0
  36. package/dist/{model-catalog.d.ts → cli/model-catalog.d.ts} +6 -0
  37. package/dist/cli/model-catalog.d.ts.map +1 -0
  38. package/dist/{model-catalog.js → cli/model-catalog.js} +714 -4
  39. package/dist/{models.d.ts → cli/models.d.ts} +12 -5
  40. package/dist/cli/models.d.ts.map +1 -0
  41. package/dist/{models.js → cli/models.js} +58 -57
  42. package/dist/cli/open-browser.d.ts.map +1 -0
  43. package/dist/cli/openai-speech-provider.d.ts +3 -0
  44. package/dist/cli/openai-speech-provider.d.ts.map +1 -0
  45. package/dist/cli/openai-speech-provider.js +28 -0
  46. package/dist/cli/separation-generate.d.ts +34 -0
  47. package/dist/cli/separation-generate.d.ts.map +1 -0
  48. package/dist/cli/separation-generate.js +117 -0
  49. package/dist/cli/speech-catalog.d.ts +35 -0
  50. package/dist/cli/speech-catalog.d.ts.map +1 -0
  51. package/dist/cli/speech-catalog.js +195 -0
  52. package/dist/cli/speech-generate.d.ts +86 -0
  53. package/dist/cli/speech-generate.d.ts.map +1 -0
  54. package/dist/cli/speech-generate.js +156 -0
  55. package/dist/cli/speech-models.d.ts +8 -0
  56. package/dist/cli/speech-models.d.ts.map +1 -0
  57. package/dist/cli/speech-models.js +109 -0
  58. package/dist/cli/speech-provider.test.d.ts +2 -0
  59. package/dist/cli/speech-provider.test.d.ts.map +1 -0
  60. package/dist/cli/speech-provider.test.js +197 -0
  61. package/dist/cli/subscription.d.ts.map +1 -0
  62. package/dist/cli/transcription-catalog.d.ts +35 -0
  63. package/dist/cli/transcription-catalog.d.ts.map +1 -0
  64. package/dist/cli/transcription-catalog.js +192 -0
  65. package/dist/cli/transcription-generate.d.ts +62 -0
  66. package/dist/cli/transcription-generate.d.ts.map +1 -0
  67. package/dist/cli/transcription-generate.js +144 -0
  68. package/dist/cli/transcription-models.d.ts +9 -0
  69. package/dist/cli/transcription-models.d.ts.map +1 -0
  70. package/dist/cli/transcription-models.js +100 -0
  71. package/dist/cli/transcription-providers.d.ts +22 -0
  72. package/dist/cli/transcription-providers.d.ts.map +1 -0
  73. package/dist/cli/transcription-providers.js +290 -0
  74. package/dist/cli/video-model-catalog.d.ts.map +1 -0
  75. package/dist/cli/xai-auth.d.ts.map +1 -0
  76. package/dist/{xai-auth.js → cli/xai-auth.js} +1 -1
  77. package/dist/vite/angled-screen.d.ts +57 -0
  78. package/dist/vite/angled-screen.d.ts.map +1 -0
  79. package/dist/vite/angled-screen.js +115 -0
  80. package/dist/vite/app.d.ts +51 -0
  81. package/dist/vite/app.d.ts.map +1 -0
  82. package/dist/vite/app.js +150 -0
  83. package/dist/vite/bands-shader.d.ts +47 -0
  84. package/dist/vite/bands-shader.d.ts.map +1 -0
  85. package/dist/vite/bands-shader.js +267 -0
  86. package/dist/vite/code-block.d.ts +107 -0
  87. package/dist/vite/code-block.d.ts.map +1 -0
  88. package/dist/vite/code-block.js +641 -0
  89. package/dist/vite/components.d.ts +36 -0
  90. package/dist/vite/components.d.ts.map +1 -0
  91. package/dist/vite/components.js +113 -0
  92. package/dist/vite/dispersion-rings-shader.d.ts +44 -0
  93. package/dist/vite/dispersion-rings-shader.d.ts.map +1 -0
  94. package/dist/vite/dispersion-rings-shader.js +282 -0
  95. package/dist/vite/easing-curves.d.ts +129 -0
  96. package/dist/vite/easing-curves.d.ts.map +1 -0
  97. package/dist/vite/easing-curves.js +584 -0
  98. package/dist/vite/easing-curves.test.d.ts +2 -0
  99. package/dist/vite/easing-curves.test.d.ts.map +1 -0
  100. package/dist/vite/easing-curves.test.js +200 -0
  101. package/dist/vite/generated-media-client.d.ts +35 -0
  102. package/dist/vite/generated-media-client.d.ts.map +1 -0
  103. package/dist/vite/generated-media-client.js +76 -0
  104. package/dist/vite/keyframes.d.ts +129 -0
  105. package/dist/vite/keyframes.d.ts.map +1 -0
  106. package/dist/vite/keyframes.js +194 -0
  107. package/dist/vite/layout-editor.d.ts +45 -0
  108. package/dist/vite/layout-editor.d.ts.map +1 -0
  109. package/dist/vite/layout-editor.js +587 -0
  110. package/dist/vite/layout-transition.d.ts +136 -0
  111. package/dist/vite/layout-transition.d.ts.map +1 -0
  112. package/dist/vite/layout-transition.js +581 -0
  113. package/dist/vite/liquid-gradient-shader.d.ts +62 -0
  114. package/dist/vite/liquid-gradient-shader.d.ts.map +1 -0
  115. package/dist/vite/liquid-gradient-shader.js +413 -0
  116. package/dist/vite/mdx-client.d.ts +15 -0
  117. package/dist/vite/mdx-client.d.ts.map +1 -0
  118. package/dist/vite/mdx-client.js +265 -0
  119. package/dist/vite/mdx-parse.d.ts +102 -0
  120. package/dist/vite/mdx-parse.d.ts.map +1 -0
  121. package/dist/vite/mdx-parse.js +251 -0
  122. package/dist/vite/mdx-provided-components.d.ts +18 -0
  123. package/dist/vite/mdx-provided-components.d.ts.map +1 -0
  124. package/dist/vite/mdx-provided-components.js +14 -0
  125. package/dist/vite/mdx-video.d.ts +327 -0
  126. package/dist/vite/mdx-video.d.ts.map +1 -0
  127. package/dist/vite/mdx-video.js +596 -0
  128. package/dist/vite/mdx-video.test.d.ts +9 -0
  129. package/dist/vite/mdx-video.test.d.ts.map +1 -0
  130. package/dist/vite/mdx-video.test.js +1945 -0
  131. package/dist/vite/media-components.d.ts +45 -0
  132. package/dist/vite/media-components.d.ts.map +1 -0
  133. package/dist/vite/media-components.js +355 -0
  134. package/dist/vite/media-duration-store.d.ts +66 -0
  135. package/dist/vite/media-duration-store.d.ts.map +1 -0
  136. package/dist/vite/media-duration-store.js +204 -0
  137. package/dist/vite/player-page.d.ts +35 -0
  138. package/dist/vite/player-page.d.ts.map +1 -0
  139. package/dist/vite/player-page.js +720 -0
  140. package/dist/vite/render-client.d.ts +18 -0
  141. package/dist/vite/render-client.d.ts.map +1 -0
  142. package/dist/vite/render-client.js +67 -0
  143. package/dist/vite/sdk.d.ts +221 -0
  144. package/dist/vite/sdk.d.ts.map +1 -0
  145. package/dist/vite/sdk.js +355 -0
  146. package/dist/vite/server-components.d.ts +55 -0
  147. package/dist/vite/server-components.d.ts.map +1 -0
  148. package/dist/vite/server-components.js +156 -0
  149. package/dist/vite/server-mdx.d.ts +74 -0
  150. package/dist/vite/server-mdx.d.ts.map +1 -0
  151. package/dist/vite/server-mdx.js +259 -0
  152. package/dist/vite/shader-renderer.d.ts +48 -0
  153. package/dist/vite/shader-renderer.d.ts.map +1 -0
  154. package/dist/vite/shader-renderer.js +411 -0
  155. package/dist/vite/store-hooks.d.ts +12 -0
  156. package/dist/vite/store-hooks.d.ts.map +1 -0
  157. package/dist/vite/store-hooks.js +93 -0
  158. package/dist/vite/store.d.ts +75 -0
  159. package/dist/vite/store.d.ts.map +1 -0
  160. package/dist/vite/store.js +52 -0
  161. package/dist/vite/store.test.d.ts +6 -0
  162. package/dist/vite/store.test.d.ts.map +1 -0
  163. package/dist/vite/store.test.js +172 -0
  164. package/dist/vite/tweakpane-hook.d.ts +94 -0
  165. package/dist/vite/tweakpane-hook.d.ts.map +1 -0
  166. package/dist/vite/tweakpane-hook.js +450 -0
  167. package/dist/vite/vite-plugin.d.ts +28 -0
  168. package/dist/vite/vite-plugin.d.ts.map +1 -0
  169. package/dist/vite/vite-plugin.js +538 -0
  170. package/dist/vite/wave-gradient-shader.d.ts +41 -0
  171. package/dist/vite/wave-gradient-shader.d.ts.map +1 -0
  172. package/dist/vite/wave-gradient-shader.js +207 -0
  173. package/package.json +101 -24
  174. package/skills/egaki/SKILL.md +162 -1
  175. package/src/cli/cache-utils.ts +169 -0
  176. package/src/cli/cached-generate.test.ts +531 -0
  177. package/src/cli/cached-generate.ts +348 -0
  178. package/src/cli/cartesia-provider.ts +249 -0
  179. package/src/{cli.ts → cli/cli.ts} +836 -884
  180. package/src/cli/clone-generate.ts +202 -0
  181. package/src/{credentials.ts → cli/credentials.ts} +20 -0
  182. package/src/cli/elevenlabs-speech-provider.ts +243 -0
  183. package/src/cli/generate.test.ts +166 -0
  184. package/src/cli/generate.ts +912 -0
  185. package/src/cli/main.ts +8 -0
  186. package/src/{model-catalog.ts → cli/model-catalog.ts} +787 -58
  187. package/src/{models.ts → cli/models.ts} +65 -86
  188. package/src/cli/openai-speech-provider.ts +34 -0
  189. package/src/cli/separation-generate.ts +187 -0
  190. package/src/cli/speech-catalog.ts +253 -0
  191. package/src/cli/speech-generate.ts +275 -0
  192. package/src/cli/speech-models.ts +146 -0
  193. package/src/cli/speech-provider.test.ts +226 -0
  194. package/src/cli/transcription-catalog.ts +250 -0
  195. package/src/cli/transcription-generate.ts +214 -0
  196. package/src/cli/transcription-models.ts +136 -0
  197. package/src/cli/transcription-providers.ts +427 -0
  198. package/src/{xai-auth.ts → cli/xai-auth.ts} +1 -1
  199. package/src/vite/angled-screen.tsx +214 -0
  200. package/src/vite/app.tsx +197 -0
  201. package/src/vite/bands-shader.tsx +319 -0
  202. package/src/vite/code-block.tsx +939 -0
  203. package/src/vite/components.tsx +278 -0
  204. package/src/vite/dispersion-rings-shader.tsx +331 -0
  205. package/src/vite/easing-curves.test.ts +232 -0
  206. package/src/vite/easing-curves.ts +743 -0
  207. package/src/vite/generated-media-client.tsx +121 -0
  208. package/src/vite/keyframes.tsx +347 -0
  209. package/src/vite/layout-editor.tsx +709 -0
  210. package/src/vite/layout-transition.tsx +706 -0
  211. package/src/vite/liquid-gradient-shader.tsx +474 -0
  212. package/src/vite/mdx-client.tsx +420 -0
  213. package/src/vite/mdx-parse.ts +343 -0
  214. package/src/vite/mdx-provided-components.ts +19 -0
  215. package/src/vite/mdx-video.test.tsx +2219 -0
  216. package/src/vite/mdx-video.tsx +976 -0
  217. package/src/vite/media-components.tsx +473 -0
  218. package/src/vite/media-duration-store.ts +225 -0
  219. package/src/vite/motion-timing.ts +194 -0
  220. package/src/vite/player-page.tsx +1107 -0
  221. package/src/vite/render-client.ts +84 -0
  222. package/src/vite/sdk.ts +598 -0
  223. package/src/vite/server-components.tsx +241 -0
  224. package/src/vite/server-mdx.ts +285 -0
  225. package/src/vite/shader-renderer.tsx +535 -0
  226. package/src/vite/store-hooks.ts +117 -0
  227. package/src/vite/store.test.ts +202 -0
  228. package/src/vite/store.ts +115 -0
  229. package/src/vite/styles.css +21 -0
  230. package/src/vite/tweakpane-hook.tsx +593 -0
  231. package/src/vite/virtual-modules.d.ts +30 -0
  232. package/src/vite/vite-plugin.ts +584 -0
  233. package/src/vite/wave-gradient-shader.tsx +253 -0
  234. package/README.md +0 -370
  235. package/dist/antigravity-auth.d.ts +0 -15
  236. package/dist/antigravity-auth.d.ts.map +0 -1
  237. package/dist/antigravity-auth.js +0 -544
  238. package/dist/antigravity-image.d.ts +0 -11
  239. package/dist/antigravity-image.d.ts.map +0 -1
  240. package/dist/antigravity-image.js +0 -285
  241. package/dist/chatgpt-auth.d.ts.map +0 -1
  242. package/dist/cli.d.ts +0 -3
  243. package/dist/cli.d.ts.map +0 -1
  244. package/dist/credentials.d.ts.map +0 -1
  245. package/dist/gateway-model-candidates.generated.d.ts +0 -11
  246. package/dist/gateway-model-candidates.generated.d.ts.map +0 -1
  247. package/dist/gateway-model-candidates.generated.js +0 -64
  248. package/dist/login.d.ts.map +0 -1
  249. package/dist/model-catalog.d.ts.map +0 -1
  250. package/dist/models.d.ts.map +0 -1
  251. package/dist/open-browser.d.ts.map +0 -1
  252. package/dist/subscription.d.ts.map +0 -1
  253. package/dist/video-model-catalog.d.ts.map +0 -1
  254. package/dist/xai-auth.d.ts.map +0 -1
  255. /package/dist/{chatgpt-auth.d.ts → cli/chatgpt-auth.d.ts} +0 -0
  256. /package/dist/{chatgpt-auth.js → cli/chatgpt-auth.js} +0 -0
  257. /package/dist/{credentials.d.ts → cli/credentials.d.ts} +0 -0
  258. /package/dist/{login.d.ts → cli/login.d.ts} +0 -0
  259. /package/dist/{login.js → cli/login.js} +0 -0
  260. /package/dist/{open-browser.d.ts → cli/open-browser.d.ts} +0 -0
  261. /package/dist/{open-browser.js → cli/open-browser.js} +0 -0
  262. /package/dist/{subscription.d.ts → cli/subscription.d.ts} +0 -0
  263. /package/dist/{subscription.js → cli/subscription.js} +0 -0
  264. /package/dist/{video-model-catalog.d.ts → cli/video-model-catalog.d.ts} +0 -0
  265. /package/dist/{video-model-catalog.js → cli/video-model-catalog.js} +0 -0
  266. /package/dist/{xai-auth.d.ts → cli/xai-auth.d.ts} +0 -0
  267. /package/src/{chatgpt-auth.ts → cli/chatgpt-auth.ts} +0 -0
  268. /package/src/{login.ts → cli/login.ts} +0 -0
  269. /package/src/{open-browser.ts → cli/open-browser.ts} +0 -0
  270. /package/src/{subscription.ts → cli/subscription.ts} +0 -0
  271. /package/src/{video-model-catalog.ts → cli/video-model-catalog.ts} +0 -0
@@ -0,0 +1,912 @@
1
+ // Programmatic API for egaki image and video generation.
2
+ // Returns Error | Result (errore style) instead of process.exit/console.error.
3
+ //
4
+ // Two variants for each function:
5
+ // - `generateImage(opts)` — cached, writes to public/generated/, returns { src: string }
6
+ // - `generateImageUncached(opts)` — raw, returns bytes in GenerateImageResult
7
+ //
8
+ // The CLI uses *Uncached variants. The cached variants are for the Vite MDX
9
+ // video framework and .server.tsx components.
10
+ //
11
+ // Usage:
12
+ // import { generateImage, generateImageUncached } from 'egaki/generate'
13
+ // const cached = await generateImage({ prompt: 'a sunset' })
14
+ // if (!(cached instanceof Error)) cached.src // '/generated/image/sunset-a1b2c3d4.png'
15
+ // const raw = await generateImageUncached({ prompt: 'a sunset' })
16
+ // if (!(raw instanceof Error)) raw.images[0].uint8Array // raw bytes
17
+ import {
18
+ generateImage as aiGenerateImage,
19
+ generateText,
20
+ experimental_generateVideo as aiGenerateVideo,
21
+ } from 'ai'
22
+ import type { XaiImageModelOptions, XaiVideoModelOptions } from '@ai-sdk/xai'
23
+ import type { GoogleImageModelOptions, GoogleLanguageModelOptions, GoogleVideoModelOptions } from '@ai-sdk/google'
24
+ import type { GoogleVertexImageModelOptions, GoogleVertexVideoModelOptions } from '@ai-sdk/google-vertex'
25
+ import type { FalImageModelOptions, FalVideoModelOptions } from '@ai-sdk/fal'
26
+ import type { ByteDanceVideoProviderOptions } from '@ai-sdk/bytedance'
27
+ import { createParser, type EventSourceMessage } from 'eventsource-parser'
28
+ import {
29
+ injectCredentialsToEnv,
30
+ getChatGptAuth,
31
+ saveChatGptAuth,
32
+ } from './credentials.js'
33
+ import {
34
+ getModelConfig,
35
+ createImageModel,
36
+ createTextModel,
37
+ createVideoModel,
38
+ shouldUseResponsesApi,
39
+ DEFAULT_MODEL,
40
+ DEFAULT_VIDEO_MODEL,
41
+ ValidationError,
42
+ } from './models.js'
43
+
44
+ export { ValidationError }
45
+ import { getValidChatGptAuth } from './chatgpt-auth.js'
46
+
47
+ // ─── autocomplete-friendly union types ───────────────────────────────────────
48
+ // `(string & {})` lets users pass arbitrary strings (new models, custom
49
+ // endpoints) while still providing autocomplete for known values.
50
+
51
+ /** Image model IDs from the catalog. Accepts arbitrary strings too. */
52
+ export type ImageModelId =
53
+ | 'imagen-4.0-generate-001' | 'imagen-4.0-ultra-generate-001' | 'imagen-4.0-fast-generate-001'
54
+ | 'gemini-2.0-flash-exp-image-generation' | 'gemini-2.5-flash-image' | 'gemini-3-pro-image-preview' | 'gemini-3.1-flash-image-preview'
55
+ | 'vertex/imagen-4.0-generate-001' | 'vertex/imagen-4.0-ultra-generate-001' | 'vertex/imagen-4.0-fast-generate-001'
56
+ | 'vertex/gemini-2.5-flash-image' | 'vertex/gemini-3-pro-image-preview' | 'vertex/gemini-3.1-flash-image-preview'
57
+ | 'flux-kontext-max' | 'flux-kontext-pro' | 'flux-pro-1.0-fill' | 'flux-pro-1.1' | 'flux-pro-1.1-ultra'
58
+ | 'flux-2-pro' | 'flux-2-max' | 'flux-2-flex' | 'flux-2-klein-9b' | 'flux-2-klein-4b'
59
+ | 'recraft-v2' | 'recraft-v3' | 'recraft-v4' | 'recraft-v4-pro' | 'recraft-v4.1' | 'recraft-v4.1-pro' | 'recraft-v4.1-utility' | 'recraft-v4.1-utility-pro'
60
+ | 'seedream-5.0-lite' | 'seedream-4.5' | 'seedream-4.0'
61
+ | 'grok-imagine-image' | 'grok-imagine-image-quality'
62
+ | 'dall-e-2' | 'dall-e-3' | 'gpt-image-1' | 'gpt-image-1-mini' | 'gpt-image-1.5' | 'gpt-image-2' | 'chatgpt-image-latest'
63
+ | (string & {})
64
+
65
+ /** Video model IDs from the catalog. Accepts arbitrary strings too. */
66
+ export type VideoModelId =
67
+ | 'veo-3.1-generate-001' | 'veo-3.1-fast-generate-001' | 'veo-3.0-generate-001' | 'veo-3.0-fast-generate-001'
68
+ | 'vertex/veo-3.1-generate-001' | 'vertex/veo-3.1-fast-generate-001'
69
+ | 'grok-imagine-video' | 'grok-imagine-video-1.5'
70
+ | 'kling-v3.0-t2v' | 'kling-v3.0-i2v' | 'kling-v3.0-motion-control' | 'kling-v2.6-t2v' | 'kling-v2.6-i2v' | 'kling-v2.6-motion-control'
71
+ | 'kling-v2.5-turbo-t2v' | 'kling-v2.5-turbo-i2v'
72
+ | 'wan-v2.6-t2v' | 'wan-v2.6-i2v' | 'wan-v2.6-i2v-flash' | 'wan-v2.6-r2v' | 'wan-v2.6-r2v-flash' | 'wan-v2.5-t2v-preview'
73
+ | 'seedance-2.0' | 'seedance-2.0-fast' | 'seedance-v1.5-pro' | 'seedance-v1.0-pro' | 'seedance-v1.0-pro-fast' | 'seedance-v1.0-lite-t2v' | 'seedance-v1.0-lite-i2v'
74
+ | 'luma-ray-2' | 'minimax-video' | 'hunyuan-video'
75
+ | (string & {})
76
+
77
+ /** Common aspect ratios across providers. */
78
+ export type AspectRatio =
79
+ | '1:1' | '3:4' | '4:3' | '9:16' | '16:9' | '2:3' | '3:2' | '4:5' | '5:4' | '9:21' | '21:9'
80
+ | (string & {})
81
+
82
+ /** Image quality presets (varies by provider). */
83
+ export type ImageQuality = 'auto' | 'low' | 'medium' | 'high' | 'standard' | (string & {})
84
+
85
+ /** Image output formats. */
86
+ export type ImageOutputFormat = 'png' | 'jpeg' | 'webp' | (string & {})
87
+
88
+ // ─── public types ────────────────────────────────────────────────────────────
89
+
90
+ export type GeneratedFile = {
91
+ uint8Array: Uint8Array
92
+ mediaType: string
93
+ }
94
+
95
+ export interface GenerateImageOptions {
96
+ prompt: string
97
+ /** Model ID. Defaults to DEFAULT_MODEL if omitted. */
98
+ model?: ImageModelId
99
+ count?: number
100
+ aspectRatio?: AspectRatio
101
+ seed?: number
102
+ inputImages?: Uint8Array[]
103
+ maskImage?: Uint8Array
104
+ allowPeople?: boolean
105
+ quality?: ImageQuality
106
+ resolution?: string
107
+ outputFormat?: ImageOutputFormat
108
+ negativePrompt?: string
109
+ /** Only for text-model image generation (Gemini). */
110
+ imageSize?: '1K' | '2K' | '4K'
111
+ }
112
+
113
+ export interface GenerateImageResult {
114
+ images: GeneratedFile[]
115
+ model: string
116
+ cost: number | null
117
+ usage?: { inputTokens?: number; outputTokens?: number }
118
+ warnings?: unknown[]
119
+ /** Text response from text-model path (Gemini). */
120
+ text?: string
121
+ /** Revised prompt from ChatGPT Responses API path. */
122
+ revisedPrompt?: string | null
123
+ }
124
+
125
+ export interface GenerateVideoOptions {
126
+ prompt: string
127
+ /** Model ID. Defaults to DEFAULT_VIDEO_MODEL if omitted. */
128
+ model?: VideoModelId
129
+ count?: number
130
+ aspectRatio?: AspectRatio
131
+ resolution?: string
132
+ duration?: number
133
+ fps?: number
134
+ seed?: number
135
+ inputImage?: Uint8Array
136
+ mode?: 'edit-video' | 'extend-video' | 'reference-to-video'
137
+ /** URL for video editing/extension input. Must be a URL (not a local path). */
138
+ videoUrl?: string
139
+ /** Reference image URLs for r2v generation. Must be URLs (not local paths). */
140
+ referenceImages?: string[]
141
+ negativePrompt?: string
142
+ }
143
+
144
+ export interface GenerateVideoResult {
145
+ videos: GeneratedFile[]
146
+ model: string
147
+ cost: number | null
148
+ warnings?: unknown[]
149
+ /** Raw responses from the AI SDK (provider-specific). */
150
+ responses?: unknown[]
151
+ }
152
+
153
+ // ─── OpenAI provider options type (not exported by @ai-sdk/openai) ───────────
154
+
155
+ type OpenAIImageProviderOptions = {
156
+ quality?: 'standard' | 'low' | 'medium' | 'high' | 'auto'
157
+ outputFormat?: 'png' | 'jpeg' | 'webp'
158
+ outputCompression?: number
159
+ size?: `${number}x${number}`
160
+ partialImages?: number | null
161
+ background?: 'auto' | 'opaque' | 'transparent'
162
+ }
163
+
164
+ // ─── type-safe provider option builders ──────────────────────────────────────
165
+
166
+ type ProviderOptionsResult = Record<string, Record<string, string | number | boolean | string[] | null | undefined | Record<string, string | number | boolean | null | undefined>>>
167
+
168
+ export function buildImageProviderOptions(
169
+ provider: string,
170
+ opts: {
171
+ aspectRatio?: `${number}:${number}`
172
+ allowPeople: boolean
173
+ quality?: string
174
+ resolution?: string
175
+ outputFormat?: string
176
+ negativePrompt?: string
177
+ },
178
+ ): ProviderOptionsResult {
179
+ switch (provider) {
180
+ case 'google': {
181
+ const googleOpts = {
182
+ ...(opts.allowPeople ? { personGeneration: 'allow_all' as const } : {}),
183
+ ...(opts.aspectRatio ? { aspectRatio: opts.aspectRatio as GoogleImageModelOptions['aspectRatio'] } : {}),
184
+ } satisfies GoogleImageModelOptions
185
+ return { google: googleOpts }
186
+ }
187
+ case 'vertex': {
188
+ const vertexOpts = {
189
+ ...(opts.allowPeople ? { personGeneration: 'allow_all' as const } : {}),
190
+ ...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
191
+ } satisfies GoogleVertexImageModelOptions
192
+ return { vertex: vertexOpts }
193
+ }
194
+ case 'xai': {
195
+ const xaiOpts = {
196
+ ...(opts.quality ? { quality: opts.quality as XaiImageModelOptions['quality'] } : {}),
197
+ ...(opts.resolution ? { resolution: opts.resolution as XaiImageModelOptions['resolution'] } : {}),
198
+ ...(opts.outputFormat ? { output_format: opts.outputFormat } : {}),
199
+ } satisfies XaiImageModelOptions
200
+ return { xai: xaiOpts }
201
+ }
202
+ case 'openai': {
203
+ const openaiOpts = {
204
+ ...(opts.quality ? { quality: opts.quality as OpenAIImageProviderOptions['quality'] } : {}),
205
+ ...(opts.outputFormat ? { outputFormat: opts.outputFormat as OpenAIImageProviderOptions['outputFormat'] } : {}),
206
+ } satisfies OpenAIImageProviderOptions
207
+ return { openai: openaiOpts }
208
+ }
209
+ case 'fal': {
210
+ const falOpts = {
211
+ ...(opts.outputFormat ? { outputFormat: opts.outputFormat } : {}),
212
+ ...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
213
+ } satisfies FalImageModelOptions
214
+ return { fal: falOpts }
215
+ }
216
+ default:
217
+ return {}
218
+ }
219
+ }
220
+
221
+ export function buildVideoProviderOptions(
222
+ provider: string,
223
+ opts: {
224
+ resolution?: string
225
+ mode?: string
226
+ videoUrl?: string
227
+ referenceImages?: string[]
228
+ negativePrompt?: string
229
+ model: string
230
+ },
231
+ ): ProviderOptionsResult | undefined {
232
+ switch (provider) {
233
+ case 'xai': {
234
+ const base = {
235
+ ...(opts.resolution ? { resolution: opts.resolution as '480p' | '720p' } : {}),
236
+ }
237
+ if (opts.mode === 'edit-video' && opts.videoUrl) {
238
+ const xaiOpts = { ...base, mode: 'edit-video' as const, videoUrl: opts.videoUrl } satisfies XaiVideoModelOptions
239
+ return { xai: xaiOpts }
240
+ }
241
+ if (opts.mode === 'extend-video' && opts.videoUrl) {
242
+ const xaiOpts = { ...base, mode: 'extend-video' as const, videoUrl: opts.videoUrl } satisfies XaiVideoModelOptions
243
+ return { xai: xaiOpts }
244
+ }
245
+ if (opts.mode === 'reference-to-video' && opts.referenceImages) {
246
+ const xaiOpts = { ...base, mode: 'reference-to-video' as const, referenceImageUrls: opts.referenceImages } satisfies XaiVideoModelOptions
247
+ return { xai: xaiOpts }
248
+ }
249
+ if (Object.keys(base).length === 0) return undefined
250
+ const xaiOpts = { ...base } satisfies XaiVideoModelOptions
251
+ return { xai: xaiOpts }
252
+ }
253
+ case 'bytedance': {
254
+ const bytedanceOpts = {
255
+ ...(opts.resolution ? { resolution: opts.resolution } : {}),
256
+ ...(opts.referenceImages ? { referenceImages: opts.referenceImages } : {}),
257
+ } satisfies ByteDanceVideoProviderOptions
258
+ if (Object.keys(bytedanceOpts).length === 0) return undefined
259
+ return { bytedance: bytedanceOpts }
260
+ }
261
+ case 'fal': {
262
+ const falOpts = {
263
+ ...(opts.resolution ? { resolution: opts.resolution } : {}),
264
+ ...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
265
+ } satisfies FalVideoModelOptions
266
+ if (Object.keys(falOpts).length === 0) return undefined
267
+ return { fal: falOpts }
268
+ }
269
+ case 'google': {
270
+ const googleOpts = {
271
+ ...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
272
+ } satisfies GoogleVideoModelOptions
273
+ if (Object.keys(googleOpts).length === 0) return undefined
274
+ return { google: googleOpts }
275
+ }
276
+ case 'vertex': {
277
+ const vertexOpts = {
278
+ ...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
279
+ } satisfies GoogleVertexVideoModelOptions
280
+ if (Object.keys(vertexOpts).length === 0) return undefined
281
+ return { vertex: vertexOpts }
282
+ }
283
+ default:
284
+ return undefined
285
+ }
286
+ }
287
+
288
+ // ─── cost calculation ────────────────────────────────────────────────────────
289
+
290
+ export function calculateCost(
291
+ cost: {
292
+ type: 'per-image'
293
+ perImage: number
294
+ } | {
295
+ type: 'per-token'
296
+ inputPerM: number
297
+ outputPerM: number
298
+ } | {
299
+ type: 'per-video-second'
300
+ defaultDurationSec: number
301
+ tiers: Array<{ resolution?: string; costPerSecond: number; hasVideoInput?: boolean }>
302
+ } | {
303
+ type: 'unknown'
304
+ },
305
+ usage: {
306
+ inputTokens?: number
307
+ outputTokens?: number
308
+ imagesGenerated?: number
309
+ videosGenerated?: number
310
+ durationSeconds?: number
311
+ resolution?: string
312
+ hasVideoInput?: boolean
313
+ },
314
+ count: number = 1,
315
+ ): number | null {
316
+ if (cost.type === 'per-image') {
317
+ return cost.perImage * count
318
+ }
319
+ if (cost.type === 'per-token' && usage.inputTokens != null && usage.outputTokens != null) {
320
+ return (
321
+ (usage.inputTokens * cost.inputPerM + usage.outputTokens * cost.outputPerM) / 1_000_000
322
+ )
323
+ }
324
+ if (cost.type === 'per-video-second') {
325
+ const durationSec = usage.durationSeconds ?? cost.defaultDurationSec
326
+ const resolution = normalizeResolutionKey(usage.resolution)
327
+ const hasVideoInput = usage.hasVideoInput ?? false
328
+ const tier =
329
+ cost.tiers.find((t) =>
330
+ normalizeResolutionKey(t.resolution) === resolution &&
331
+ (t.hasVideoInput == null || t.hasVideoInput === hasVideoInput),
332
+ ) ??
333
+ cost.tiers.find((t) => normalizeResolutionKey(t.resolution) === resolution) ??
334
+ cost.tiers[0]
335
+ if (!tier) {
336
+ return null
337
+ }
338
+ return tier.costPerSecond * durationSec * count
339
+ }
340
+ return null
341
+ }
342
+
343
+ function normalizeResolutionKey(input?: string): string | undefined {
344
+ if (!input) return undefined
345
+ const normalized = input.trim().toLowerCase()
346
+ if (normalized === '1920x1080') return '1080p'
347
+ if (normalized === '1280x720') return '720p'
348
+ if (normalized === '854x480' || normalized === '848x480') return '480p'
349
+ return normalized
350
+ }
351
+
352
+ // ─── image byte helpers ──────────────────────────────────────────────────────
353
+
354
+ function inferImageMediaType(image: Uint8Array): string {
355
+ if (image.length >= 8 && image[0] === 0x89 && image[1] === 0x50 && image[2] === 0x4e && image[3] === 0x47) {
356
+ return 'image/png'
357
+ }
358
+ if (image.length >= 3 && image[0] === 0xff && image[1] === 0xd8 && image[2] === 0xff) {
359
+ return 'image/jpeg'
360
+ }
361
+ if (
362
+ image.length >= 12 &&
363
+ image[0] === 0x52 && image[1] === 0x49 && image[2] === 0x46 && image[3] === 0x46 &&
364
+ image[8] === 0x57 && image[9] === 0x45 && image[10] === 0x42 && image[11] === 0x50
365
+ ) {
366
+ return 'image/webp'
367
+ }
368
+ if (
369
+ image.length >= 6 &&
370
+ image[0] === 0x47 && image[1] === 0x49 && image[2] === 0x46 && image[3] === 0x38
371
+ ) {
372
+ return 'image/gif'
373
+ }
374
+ return 'image/png'
375
+ }
376
+
377
+ function imageBytesToDataUrl(image: Uint8Array): string {
378
+ const mediaType = inferImageMediaType(image)
379
+ return `data:${mediaType};base64,${Buffer.from(image).toString('base64')}`
380
+ }
381
+
382
+ function chatGptImageSizeFromAspectRatio(
383
+ aspectRatio: `${number}:${number}`,
384
+ ): '1024x1024' | '1536x1024' | '1024x1536' | undefined {
385
+ switch (aspectRatio) {
386
+ case '1:1':
387
+ return '1024x1024'
388
+ case '3:2':
389
+ return '1536x1024'
390
+ case '2:3':
391
+ return '1024x1536'
392
+ default:
393
+ return undefined
394
+ }
395
+ }
396
+
397
+ function isAspectRatio(input: string): input is `${number}:${number}` {
398
+ return /^\d+:\d+$/.test(input)
399
+ }
400
+
401
+ /** Convert any thrown value into an Error instance. */
402
+ function toError(err: unknown): Error {
403
+ if (err instanceof Error) return err
404
+ return new Error(String(err))
405
+ }
406
+
407
+ // ─── generateImage ───────────────────────────────────────────────────────────
408
+
409
+ /**
410
+ * Generate images from a text prompt. Auto-detects which generation strategy
411
+ * to use (image API, text model, or ChatGPT Responses API) based on model ID.
412
+ * Shares credentials and subscription with the CLI.
413
+ */
414
+ export async function generateImageUncached(opts: GenerateImageOptions): Promise<Error | GenerateImageResult> {
415
+ injectCredentialsToEnv()
416
+
417
+ const model = opts.model ?? DEFAULT_MODEL
418
+ const count = opts.count ?? 1
419
+ const config = getModelConfig(model)
420
+ if (config instanceof Error) return config
421
+
422
+ const parsedAspectRatio = opts.aspectRatio && isAspectRatio(opts.aspectRatio)
423
+ ? opts.aspectRatio
424
+ : undefined
425
+ const inputImages = opts.inputImages ?? []
426
+
427
+ // ChatGPT OAuth + OpenAI image models → Responses API path
428
+ const useResponsesApi = config.strategy === 'image' && shouldUseResponsesApi(model)
429
+
430
+ if (useResponsesApi) {
431
+ return generateWithResponsesApi({
432
+ prompt: opts.prompt,
433
+ model,
434
+ count,
435
+ aspectRatio: parsedAspectRatio,
436
+ seed: opts.seed,
437
+ maskImage: opts.maskImage,
438
+ inputImages,
439
+ })
440
+ }
441
+
442
+ if (config.strategy === 'image') {
443
+ return generateWithImageModel({
444
+ prompt: opts.prompt,
445
+ model,
446
+ count,
447
+ aspectRatio: parsedAspectRatio,
448
+ seed: opts.seed,
449
+ inputImages,
450
+ maskImage: opts.maskImage,
451
+ allowPeople: opts.allowPeople ?? false,
452
+ quality: opts.quality,
453
+ resolution: opts.resolution,
454
+ outputFormat: opts.outputFormat,
455
+ negativePrompt: opts.negativePrompt,
456
+ })
457
+ }
458
+
459
+ // Text model path (Gemini multimodal)
460
+ return generateWithTextModelPath({
461
+ prompt: opts.prompt,
462
+ model,
463
+ inputImages,
464
+ imageSize: opts.imageSize,
465
+ aspectRatio: opts.aspectRatio,
466
+ })
467
+ }
468
+
469
+ // ─── generateVideo ───────────────────────────────────────────────────────────
470
+
471
+ /**
472
+ * Generate videos from a text prompt (or image+text for i2v models).
473
+ * Shares credentials and subscription with the CLI.
474
+ */
475
+ export async function generateVideoUncached(opts: GenerateVideoOptions): Promise<Error | GenerateVideoResult> {
476
+ injectCredentialsToEnv()
477
+
478
+ const model = opts.model ?? DEFAULT_VIDEO_MODEL
479
+ const count = opts.count ?? 1
480
+ const config = getModelConfig(model)
481
+ if (config instanceof Error) return config
482
+
483
+ if (config.strategy !== 'video') {
484
+ return new ValidationError(`Model ${model} is not a video model`)
485
+ }
486
+
487
+ const isVideoInputMode = opts.mode === 'edit-video' || opts.mode === 'extend-video'
488
+
489
+ // Validate mode-specific required inputs
490
+ if (isVideoInputMode && !opts.videoUrl) {
491
+ return new ValidationError(`videoUrl is required with mode ${opts.mode}`)
492
+ }
493
+ if (opts.mode === 'reference-to-video' && (!opts.referenceImages || opts.referenceImages.length === 0)) {
494
+ return new ValidationError('referenceImages is required with mode reference-to-video')
495
+ }
496
+ if (opts.referenceImages?.length && !config.providerOptions?.some((opt) => opt.flag === 'reference-images')) {
497
+ return new ValidationError(`Model ${model} does not support referenceImages`)
498
+ }
499
+ if (opts.videoUrl && !config.providerOptions?.some((opt) => opt.flag === 'video-url')) {
500
+ return new ValidationError(`Model ${model} does not support video URL input for editing/extension`)
501
+ }
502
+
503
+ const videoModel = await createVideoModel(model)
504
+ if (videoModel instanceof Error) return videoModel
505
+
506
+ const providerOptions = buildVideoProviderOptions(config.provider, {
507
+ resolution: opts.resolution,
508
+ mode: opts.mode,
509
+ videoUrl: opts.videoUrl,
510
+ referenceImages: opts.referenceImages,
511
+ negativePrompt: opts.negativePrompt,
512
+ model,
513
+ })
514
+
515
+ let result
516
+ try {
517
+ result = await aiGenerateVideo({
518
+ model: videoModel,
519
+ prompt: opts.inputImage
520
+ ? { image: opts.inputImage, text: opts.prompt }
521
+ : opts.prompt,
522
+ n: count,
523
+ ...(opts.aspectRatio ? { aspectRatio: opts.aspectRatio as `${number}:${number}` } : {}),
524
+ ...(opts.resolution ? { resolution: opts.resolution as `${number}x${number}` } : {}),
525
+ ...(opts.duration != null ? { duration: opts.duration } : {}),
526
+ ...(opts.fps != null ? { fps: opts.fps } : {}),
527
+ ...(opts.seed != null ? { seed: opts.seed } : {}),
528
+ ...(providerOptions ? { providerOptions } : {}),
529
+ })
530
+ } catch (err) {
531
+ return toError(err)
532
+ }
533
+
534
+ const videos = result.videos.map((v: { uint8Array: Uint8Array; mediaType: string }) => ({
535
+ uint8Array: v.uint8Array,
536
+ mediaType: v.mediaType,
537
+ }))
538
+
539
+ const cost = calculateCost(config.cost, {
540
+ videosGenerated: result.videos.length,
541
+ durationSeconds: opts.duration,
542
+ resolution: opts.resolution,
543
+ hasVideoInput: Boolean(opts.inputImage || opts.videoUrl || opts.referenceImages?.length),
544
+ }, result.videos.length)
545
+
546
+ return {
547
+ videos,
548
+ model,
549
+ cost,
550
+ warnings: result.warnings,
551
+ responses: result.responses,
552
+ }
553
+ }
554
+
555
+ // ─── internal generation paths ───────────────────────────────────────────────
556
+
557
+ async function generateWithImageModel(opts: {
558
+ prompt: string
559
+ model: string
560
+ count: number
561
+ aspectRatio?: `${number}:${number}`
562
+ seed?: number
563
+ inputImages: Uint8Array[]
564
+ maskImage?: Uint8Array
565
+ allowPeople: boolean
566
+ quality?: string
567
+ resolution?: string
568
+ outputFormat?: string
569
+ negativePrompt?: string
570
+ }): Promise<Error | GenerateImageResult> {
571
+ const imagePrompt = opts.inputImages.length > 0
572
+ ? { text: opts.prompt, images: opts.inputImages, ...(opts.maskImage ? { mask: opts.maskImage } : {}) }
573
+ : opts.prompt
574
+
575
+ const config = getModelConfig(opts.model)
576
+ if (config instanceof Error) return config
577
+
578
+ const imageModel = await createImageModel(opts.model)
579
+ if (imageModel instanceof Error) return imageModel
580
+
581
+ const providerOptions = buildImageProviderOptions(config.provider, {
582
+ aspectRatio: opts.aspectRatio,
583
+ allowPeople: opts.allowPeople,
584
+ quality: opts.quality,
585
+ resolution: opts.resolution,
586
+ outputFormat: opts.outputFormat,
587
+ negativePrompt: opts.negativePrompt,
588
+ })
589
+
590
+ let result
591
+ try {
592
+ result = await aiGenerateImage({
593
+ model: imageModel,
594
+ prompt: imagePrompt,
595
+ n: opts.count,
596
+ ...(opts.aspectRatio ? { aspectRatio: opts.aspectRatio } : {}),
597
+ ...(opts.seed !== undefined ? { seed: opts.seed } : {}),
598
+ providerOptions,
599
+ })
600
+ } catch (err) {
601
+ return toError(err)
602
+ }
603
+
604
+ const images = result.images.map((img) => ({
605
+ uint8Array: img.uint8Array,
606
+ mediaType: img.mediaType,
607
+ }))
608
+
609
+ const cost = calculateCost(config.cost, result.usage, result.images.length)
610
+
611
+ return {
612
+ images,
613
+ model: opts.model,
614
+ cost,
615
+ usage: result.usage,
616
+ warnings: result.warnings,
617
+ }
618
+ }
619
+
620
+ async function generateWithTextModelPath(opts: {
621
+ prompt: string
622
+ model: string
623
+ inputImages: Uint8Array[]
624
+ imageSize?: '1K' | '2K' | '4K'
625
+ aspectRatio?: string
626
+ }): Promise<Error | GenerateImageResult> {
627
+ const textModel = await createTextModel(opts.model)
628
+ if (textModel instanceof Error) return textModel
629
+
630
+ const config = getModelConfig(opts.model)
631
+ if (config instanceof Error) return config
632
+
633
+ const messages = opts.inputImages.length > 0
634
+ ? [
635
+ {
636
+ role: 'user' as const,
637
+ content: [
638
+ { type: 'text' as const, text: opts.prompt },
639
+ ...opts.inputImages.map((img) => ({
640
+ type: 'image' as const,
641
+ image: img,
642
+ })),
643
+ ],
644
+ },
645
+ ]
646
+ : undefined
647
+
648
+ type GoogleAspectRatio = NonNullable<NonNullable<GoogleLanguageModelOptions['imageConfig']>['aspectRatio']>
649
+ const googleOpts = {
650
+ responseModalities: ['TEXT', 'IMAGE'],
651
+ ...(opts.imageSize || opts.aspectRatio
652
+ ? {
653
+ imageConfig: {
654
+ ...(opts.imageSize ? { imageSize: opts.imageSize } : {}),
655
+ ...(opts.aspectRatio ? { aspectRatio: opts.aspectRatio as GoogleAspectRatio } : {}),
656
+ },
657
+ }
658
+ : {}),
659
+ } satisfies GoogleLanguageModelOptions
660
+
661
+ const providerOptionsKey = config.provider === 'vertex' ? 'vertex' : 'google'
662
+
663
+ let result
664
+ try {
665
+ result = await generateText({
666
+ model: textModel,
667
+ ...(messages ? { messages } : { prompt: opts.prompt }),
668
+ providerOptions: {
669
+ [providerOptionsKey]: googleOpts,
670
+ },
671
+ })
672
+ } catch (err) {
673
+ return toError(err)
674
+ }
675
+
676
+ const imageFiles = result.files.filter((f) => f.mediaType.startsWith('image/'))
677
+ if (imageFiles.length === 0) {
678
+ return new Error('No images generated by the text model.')
679
+ }
680
+
681
+ const images = imageFiles.map((f) => ({
682
+ uint8Array: f.uint8Array,
683
+ mediaType: f.mediaType,
684
+ }))
685
+
686
+ const cost = calculateCost(config.cost, result.usage, imageFiles.length)
687
+
688
+ return {
689
+ images,
690
+ model: opts.model,
691
+ cost,
692
+ usage: result.usage,
693
+ text: result.text || undefined,
694
+ }
695
+ }
696
+
697
+ async function generateWithResponsesApi(opts: {
698
+ prompt: string
699
+ model: string
700
+ count: number
701
+ aspectRatio?: `${number}:${number}`
702
+ seed?: number
703
+ maskImage?: Uint8Array
704
+ inputImages: Uint8Array[]
705
+ }): Promise<Error | GenerateImageResult> {
706
+ if (opts.count !== 1) {
707
+ return new ValidationError('ChatGPT image generation currently supports exactly one output image.')
708
+ }
709
+
710
+ if (opts.seed !== undefined) {
711
+ return new ValidationError('ChatGPT image generation does not support seed.')
712
+ }
713
+
714
+ if (opts.maskImage) {
715
+ return new ValidationError('ChatGPT image generation does not support mask yet.')
716
+ }
717
+
718
+ const size = opts.aspectRatio ? chatGptImageSizeFromAspectRatio(opts.aspectRatio) : undefined
719
+ if (opts.aspectRatio && !size) {
720
+ return new ValidationError('ChatGPT image generation only supports aspect ratios 1:1, 3:2, or 2:3.')
721
+ }
722
+
723
+ const storedAuth = getChatGptAuth()
724
+ if (!storedAuth?.accountId) {
725
+ return new ValidationError('Missing ChatGPT account metadata. Please run `egaki login --provider chatgpt` again.')
726
+ }
727
+
728
+ const auth = await getValidChatGptAuth(storedAuth, saveChatGptAuth)
729
+ if (auth instanceof Error) return auth
730
+
731
+ const content: Array<{ type: 'input_text'; text: string } | { type: 'input_image'; image_url: string }> = [
732
+ { type: 'input_text', text: opts.prompt },
733
+ ...opts.inputImages.map((image) => ({
734
+ type: 'input_image' as const,
735
+ image_url: imageBytesToDataUrl(image),
736
+ })),
737
+ ]
738
+
739
+ let response: Response
740
+ try {
741
+ response = await fetch('https://chatgpt.com/backend-api/codex/responses', {
742
+ method: 'POST',
743
+ headers: {
744
+ Authorization: `Bearer ${auth.access}`,
745
+ ...(auth.accountId && { 'ChatGPT-Account-ID': auth.accountId }),
746
+ 'Content-Type': 'application/json',
747
+ },
748
+ body: JSON.stringify({
749
+ model: 'gpt-5.4',
750
+ instructions: 'You are Codex.',
751
+ input: [
752
+ {
753
+ type: 'message',
754
+ role: 'user',
755
+ content,
756
+ },
757
+ ],
758
+ tools: [
759
+ {
760
+ type: 'image_generation',
761
+ model: opts.model,
762
+ size: 'auto',
763
+ quality: 'auto',
764
+ output_format: 'png',
765
+ output_compression: 100,
766
+ moderation: 'auto',
767
+ ...(size ? { size } : {}),
768
+ },
769
+ ],
770
+ tool_choice: 'auto',
771
+ parallel_tool_calls: true,
772
+ stream: true,
773
+ store: false,
774
+ include: [],
775
+ }),
776
+ })
777
+ } catch (err) {
778
+ return toError(err)
779
+ }
780
+
781
+ if (!response.ok || !response.body) {
782
+ const body = await response.text().catch(() => '')
783
+ return new Error(`ChatGPT image generation failed: ${response.status}${body ? ` ${body}` : ''}`)
784
+ }
785
+
786
+ let imageBase64: string | undefined
787
+ let revisedPrompt: string | null = null
788
+
789
+ const parser = createParser({
790
+ onEvent: (message: EventSourceMessage) => {
791
+ if (!message.data) return
792
+ try {
793
+ const event = JSON.parse(message.data) as {
794
+ type?: string
795
+ partial_image_b64?: string
796
+ item?: { type?: string; result?: string; revised_prompt?: string | null }
797
+ }
798
+ if (
799
+ event.type === 'response.image_generation_call.partial_image' &&
800
+ event.partial_image_b64 &&
801
+ !imageBase64
802
+ ) {
803
+ imageBase64 = event.partial_image_b64
804
+ }
805
+ if (
806
+ event.type === 'response.output_item.done' &&
807
+ event.item?.type === 'image_generation_call'
808
+ ) {
809
+ if (event.item.result) imageBase64 = event.item.result
810
+ revisedPrompt = event.item.revised_prompt ?? revisedPrompt
811
+ }
812
+ } catch {
813
+ // Ignore keepalive and partial parse noise.
814
+ }
815
+ },
816
+ })
817
+
818
+ const decoder = new TextDecoder()
819
+ for await (const chunk of response.body as unknown as AsyncIterable<Uint8Array>) {
820
+ parser.feed(decoder.decode(chunk, { stream: true }))
821
+ }
822
+ parser.feed(decoder.decode())
823
+
824
+ if (!imageBase64) {
825
+ return new Error('No images generated by ChatGPT.')
826
+ }
827
+
828
+ const imageBytes = new Uint8Array(Buffer.from(imageBase64, 'base64'))
829
+
830
+ return {
831
+ images: [{ uint8Array: imageBytes, mediaType: 'image/png' }],
832
+ model: opts.model,
833
+ cost: null,
834
+ revisedPrompt,
835
+ }
836
+ }
837
+
838
+ // ─── cached result types ─────────────────────────────────────────────────────
839
+
840
+ export interface CachedGenerateResult {
841
+ /** Public URL path (e.g. `/generated/image/sunset-a1b2c3d4.png`) */
842
+ src: string
843
+ }
844
+
845
+ // ─── cached generateImage ───────────────────────────────────────────────────
846
+
847
+ import { cachedGenerate } from './cached-generate.js'
848
+ import { extensionFromMediaType } from './cache-utils.js'
849
+
850
+ export const generateImage = cachedGenerate<GenerateImageOptions & { seed?: number }, GeneratedFile, CachedGenerateResult>({
851
+ namespace: 'image',
852
+ prefixFrom: (p) => p.prompt,
853
+ modelFrom: (p) => p.model,
854
+ generate: async (params) => {
855
+ const result = await generateImageUncached(params)
856
+ if (result instanceof Error) throw result
857
+ const file = result.images[0]
858
+ if (!file) throw new Error('No image generated')
859
+ return file
860
+ },
861
+ serialize: (file) => ({
862
+ bytes: file.uint8Array,
863
+ extension: extensionFromMediaType(file.mediaType),
864
+ }),
865
+ })
866
+
867
+ // ─── cached generateVideo ───────────────────────────────────────────────────
868
+
869
+ export const generateVideo = cachedGenerate<GenerateVideoOptions & { seed?: number }, GeneratedFile, CachedGenerateResult>({
870
+ namespace: 'video',
871
+ prefixFrom: (p) => p.prompt,
872
+ modelFrom: (p) => p.model,
873
+ generate: async (params) => {
874
+ const result = await generateVideoUncached(params)
875
+ if (result instanceof Error) throw result
876
+ const file = result.videos[0]
877
+ if (!file) throw new Error('No video generated')
878
+ return file
879
+ },
880
+ serialize: (file) => ({
881
+ bytes: file.uint8Array,
882
+ extension: extensionFromMediaType(file.mediaType),
883
+ }),
884
+ })
885
+
886
+ // ─── re-export speech generation ─────────────────────────────────────────────
887
+
888
+ export {
889
+ generateSpeech,
890
+ generateSpeechUncached,
891
+ calculateSpeechCost,
892
+ type GenerateSpeechOptions,
893
+ type GenerateSpeechResult,
894
+ type CachedSpeechResult,
895
+ type SpeechProvider,
896
+ type SpeechProviderOptions,
897
+ type SpeechProviderResult,
898
+ } from './speech-generate.js'
899
+
900
+ // ─── re-export transcription ─────────────────────────────────────────────────
901
+
902
+ export {
903
+ transcribeAudio,
904
+ transcribeAudioUncached,
905
+ calculateTranscriptionCost,
906
+ segmentsToWordTimestamps,
907
+ wordTimestampsToCaptions,
908
+ type TranscribeOptions,
909
+ type TranscribeResult,
910
+ type WordTimestamp,
911
+ type TranscriptionSegment,
912
+ } from './transcription-generate.js'