@nodaro/shared 1.4.2

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 (665) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +32 -0
  3. package/dist/index.cjs +12506 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +12269 -0
  6. package/dist/index.d.ts +12269 -0
  7. package/dist/index.js +11831 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +54 -0
  10. package/src/__tests__/__snapshots__/prompt-builder-segments.test.ts.snap +65 -0
  11. package/src/__tests__/ai-avatar-pricing.test.ts +321 -0
  12. package/src/__tests__/apply-video-negative-prompt.test.ts +76 -0
  13. package/src/__tests__/attach-columns.test.ts +28 -0
  14. package/src/__tests__/audio-crossfade-curves.test.ts +33 -0
  15. package/src/__tests__/build-model-menu.test.ts +58 -0
  16. package/src/__tests__/character-aspect-defaults.test.ts +142 -0
  17. package/src/__tests__/character-mention-slug.test.ts +199 -0
  18. package/src/__tests__/character-reference-photo-kinds.test.ts +32 -0
  19. package/src/__tests__/character-usage-mode.test.ts +147 -0
  20. package/src/__tests__/character-variant-assets.test.ts +142 -0
  21. package/src/__tests__/character-voice.test.ts +130 -0
  22. package/src/__tests__/cinematic-avatar-pricing.test.ts +119 -0
  23. package/src/__tests__/combine-transitions.test.ts +137 -0
  24. package/src/__tests__/component-types.test.ts +87 -0
  25. package/src/__tests__/condition-variables.test.ts +98 -0
  26. package/src/__tests__/credit-identifiers.test.ts +811 -0
  27. package/src/__tests__/default-video-provider.test.ts +63 -0
  28. package/src/__tests__/entity-image-handle.test.ts +43 -0
  29. package/src/__tests__/entity-variants.test.ts +33 -0
  30. package/src/__tests__/fal-lip-sync-sets.test.ts +31 -0
  31. package/src/__tests__/featured-entities.test.ts +33 -0
  32. package/src/__tests__/film-pricing.test.ts +64 -0
  33. package/src/__tests__/filter-condition.test.ts +285 -0
  34. package/src/__tests__/freecut-protocol.test.ts +35 -0
  35. package/src/__tests__/generate-text-items.test.ts +19 -0
  36. package/src/__tests__/generated-results.test.ts +50 -0
  37. package/src/__tests__/graph-utils.test.ts +618 -0
  38. package/src/__tests__/group-aggregation.test.ts +83 -0
  39. package/src/__tests__/i18n-locale-completeness.test.ts +156 -0
  40. package/src/__tests__/image-critic-node.test.ts +86 -0
  41. package/src/__tests__/image-mask-mode.test.ts +16 -0
  42. package/src/__tests__/image-ref-limit.test.ts +78 -0
  43. package/src/__tests__/inpaint-region.test.ts +24 -0
  44. package/src/__tests__/input-field-map-parity.test.ts +30 -0
  45. package/src/__tests__/json-evaluator.test.ts +289 -0
  46. package/src/__tests__/json-path.test.ts +64 -0
  47. package/src/__tests__/lip-sync-pricing.test.ts +142 -0
  48. package/src/__tests__/list-merge.test.ts +96 -0
  49. package/src/__tests__/list-sort.test.ts +134 -0
  50. package/src/__tests__/llm-models.test.ts +445 -0
  51. package/src/__tests__/location-mention-slug.test.ts +246 -0
  52. package/src/__tests__/locations-coverage-critic-schema.test.ts +92 -0
  53. package/src/__tests__/loop-delimiter.test.ts +39 -0
  54. package/src/__tests__/lottie-slots.test.ts +245 -0
  55. package/src/__tests__/model-catalog-flux2-resolutions.test.ts +21 -0
  56. package/src/__tests__/model-constants-location.test.ts +20 -0
  57. package/src/__tests__/model-constants.test.ts +122 -0
  58. package/src/__tests__/model-tree.test.ts +86 -0
  59. package/src/__tests__/no-internal-imports.test.ts +25 -0
  60. package/src/__tests__/node-default-mappings.test.ts +175 -0
  61. package/src/__tests__/node-mappable-fields.test.ts +7 -0
  62. package/src/__tests__/node-preset-export.test.ts +46 -0
  63. package/src/__tests__/node-preset-extract.test.ts +166 -0
  64. package/src/__tests__/node-refs-canonical.test.ts +54 -0
  65. package/src/__tests__/node-refs-combine.test.ts +54 -0
  66. package/src/__tests__/node-refs.test.ts +187 -0
  67. package/src/__tests__/node-runtime-keys.test.ts +99 -0
  68. package/src/__tests__/object-aspect-defaults.test.ts +59 -0
  69. package/src/__tests__/parameter-node-value.test.ts +137 -0
  70. package/src/__tests__/pinterest-url.test.ts +35 -0
  71. package/src/__tests__/pipeline-chat.test.ts +110 -0
  72. package/src/__tests__/pipeline-validation.test.ts +98 -0
  73. package/src/__tests__/presentation-utils.test.ts +1137 -0
  74. package/src/__tests__/producer-types.test.ts +47 -0
  75. package/src/__tests__/prompt-length-limits.test.ts +156 -0
  76. package/src/__tests__/provider-directive-defaults.test.ts +46 -0
  77. package/src/__tests__/reduce-strategy-registry.test.ts +43 -0
  78. package/src/__tests__/reference-board-templates.test.ts +33 -0
  79. package/src/__tests__/reference-roles.test.ts +36 -0
  80. package/src/__tests__/resolve-field-mappings.test.ts +110 -0
  81. package/src/__tests__/resolve-pipeline-model.test.ts +76 -0
  82. package/src/__tests__/scene-node-types.test.ts +80 -0
  83. package/src/__tests__/scraper-actors.test.ts +67 -0
  84. package/src/__tests__/seedance-2-catalog.test.ts +18 -0
  85. package/src/__tests__/seedance-2-credit-id.test.ts +23 -0
  86. package/src/__tests__/seedance-aspects.test.ts +52 -0
  87. package/src/__tests__/selector.test.ts +1151 -0
  88. package/src/__tests__/settled-with-limit.test.ts +214 -0
  89. package/src/__tests__/social-media-specs.test.ts +129 -0
  90. package/src/__tests__/sub-workflow-validation.test.ts +88 -0
  91. package/src/__tests__/switchx-pricing.test.ts +28 -0
  92. package/src/__tests__/to-connected-references.test.ts +153 -0
  93. package/src/__tests__/utilities.test.ts +541 -0
  94. package/src/__tests__/video-analysis-pricing.test.ts +58 -0
  95. package/src/__tests__/video-analysis.test.ts +63 -0
  96. package/src/__tests__/video-audio-capability.test.ts +204 -0
  97. package/src/__tests__/video-gen-providers.test.ts +28 -0
  98. package/src/__tests__/video-mode-aliases.test.ts +150 -0
  99. package/src/__tests__/video-ref-limits.test.ts +72 -0
  100. package/src/__tests__/video-utils.test.ts +303 -0
  101. package/src/__tests__/voice-changer-models.test.ts +33 -0
  102. package/src/__tests__/workflow-export-strip.test.ts +46 -0
  103. package/src/ai-avatar-pricing.ts +213 -0
  104. package/src/ancestor-refs.ts +48 -0
  105. package/src/animals.ts +231 -0
  106. package/src/audio-crossfade-curves.ts +73 -0
  107. package/src/audio-fx-presets.ts +64 -0
  108. package/src/avatar-payload-validators.ts +214 -0
  109. package/src/caption-styles.ts +31 -0
  110. package/src/character-aspect-defaults.ts +102 -0
  111. package/src/character-facets.ts +37 -0
  112. package/src/character-mention-slug.ts +166 -0
  113. package/src/character-usage-mode.ts +109 -0
  114. package/src/character-variant-assets.ts +121 -0
  115. package/src/character-voice.ts +151 -0
  116. package/src/cinematic-avatar-pricing.ts +111 -0
  117. package/src/clone-utils.ts +46 -0
  118. package/src/combine-transitions.ts +482 -0
  119. package/src/community.ts +112 -0
  120. package/src/component-types.ts +127 -0
  121. package/src/condition-variables.ts +65 -0
  122. package/src/credit-estimators/index.ts +15 -0
  123. package/src/credit-estimators/video-utils.ts +165 -0
  124. package/src/credit-identifiers.ts +304 -0
  125. package/src/entity-approval-types.ts +31 -0
  126. package/src/entity-aspect-defaults.ts +82 -0
  127. package/src/entity-asset-types.ts +296 -0
  128. package/src/entity-image-handle.ts +36 -0
  129. package/src/extra-refs.ts +139 -0
  130. package/src/featured-entities.ts +136 -0
  131. package/src/film-pricing.ts +117 -0
  132. package/src/filter-condition.ts +250 -0
  133. package/src/flux2-pricing.ts +18 -0
  134. package/src/freecut-protocol.ts +52 -0
  135. package/src/furniture.ts +167 -0
  136. package/src/generate-text-items.ts +15 -0
  137. package/src/generated-results.ts +63 -0
  138. package/src/group-aggregation.ts +75 -0
  139. package/src/hint-graph-types.ts +18 -0
  140. package/src/i18n/action-fx.ar.ts +89 -0
  141. package/src/i18n/action-fx.de.ts +89 -0
  142. package/src/i18n/action-fx.es.ts +89 -0
  143. package/src/i18n/action-fx.fr.ts +89 -0
  144. package/src/i18n/action-fx.he.ts +89 -0
  145. package/src/i18n/action-fx.hi.ts +89 -0
  146. package/src/i18n/action-fx.ja.ts +89 -0
  147. package/src/i18n/action-fx.ko.ts +89 -0
  148. package/src/i18n/action-fx.pt-BR.ts +89 -0
  149. package/src/i18n/action-fx.ru.ts +89 -0
  150. package/src/i18n/action-fx.zh-CN.ts +89 -0
  151. package/src/i18n/aesthetic.ar.ts +170 -0
  152. package/src/i18n/aesthetic.de.ts +48 -0
  153. package/src/i18n/aesthetic.es.ts +169 -0
  154. package/src/i18n/aesthetic.fr.ts +52 -0
  155. package/src/i18n/aesthetic.he.ts +169 -0
  156. package/src/i18n/aesthetic.hi.ts +55 -0
  157. package/src/i18n/aesthetic.ja.ts +165 -0
  158. package/src/i18n/aesthetic.ko.ts +169 -0
  159. package/src/i18n/aesthetic.pt-BR.ts +148 -0
  160. package/src/i18n/aesthetic.ru.ts +155 -0
  161. package/src/i18n/aesthetic.zh-CN.ts +172 -0
  162. package/src/i18n/animals.ar.ts +145 -0
  163. package/src/i18n/animals.de.ts +144 -0
  164. package/src/i18n/animals.es.ts +158 -0
  165. package/src/i18n/animals.fr.ts +144 -0
  166. package/src/i18n/animals.he.ts +155 -0
  167. package/src/i18n/animals.hi.ts +157 -0
  168. package/src/i18n/animals.ja.ts +159 -0
  169. package/src/i18n/animals.ko.ts +159 -0
  170. package/src/i18n/animals.pt-BR.ts +157 -0
  171. package/src/i18n/animals.ru.ts +155 -0
  172. package/src/i18n/animals.zh-CN.ts +155 -0
  173. package/src/i18n/atmosphere.ar.ts +49 -0
  174. package/src/i18n/atmosphere.de.ts +47 -0
  175. package/src/i18n/atmosphere.es.ts +50 -0
  176. package/src/i18n/atmosphere.fr.ts +47 -0
  177. package/src/i18n/atmosphere.he.ts +47 -0
  178. package/src/i18n/atmosphere.hi.ts +49 -0
  179. package/src/i18n/atmosphere.ja.ts +49 -0
  180. package/src/i18n/atmosphere.ko.ts +49 -0
  181. package/src/i18n/atmosphere.pt-BR.ts +49 -0
  182. package/src/i18n/atmosphere.ru.ts +47 -0
  183. package/src/i18n/atmosphere.zh-CN.ts +152 -0
  184. package/src/i18n/backdrop.ar.ts +53 -0
  185. package/src/i18n/backdrop.de.ts +46 -0
  186. package/src/i18n/backdrop.es.ts +54 -0
  187. package/src/i18n/backdrop.fr.ts +52 -0
  188. package/src/i18n/backdrop.he.ts +51 -0
  189. package/src/i18n/backdrop.hi.ts +59 -0
  190. package/src/i18n/backdrop.ja.ts +61 -0
  191. package/src/i18n/backdrop.ko.ts +59 -0
  192. package/src/i18n/backdrop.pt-BR.ts +59 -0
  193. package/src/i18n/backdrop.ru.ts +57 -0
  194. package/src/i18n/backdrop.zh-CN.ts +57 -0
  195. package/src/i18n/camera-format.ar.ts +40 -0
  196. package/src/i18n/camera-format.de.ts +37 -0
  197. package/src/i18n/camera-format.es.ts +41 -0
  198. package/src/i18n/camera-format.fr.ts +37 -0
  199. package/src/i18n/camera-format.he.ts +37 -0
  200. package/src/i18n/camera-format.hi.ts +42 -0
  201. package/src/i18n/camera-format.ja.ts +37 -0
  202. package/src/i18n/camera-format.ko.ts +37 -0
  203. package/src/i18n/camera-format.pt-BR.ts +46 -0
  204. package/src/i18n/camera-format.ru.ts +42 -0
  205. package/src/i18n/camera-format.zh-CN.ts +42 -0
  206. package/src/i18n/camera-motions.ar.ts +86 -0
  207. package/src/i18n/camera-motions.de.ts +74 -0
  208. package/src/i18n/camera-motions.es.ts +86 -0
  209. package/src/i18n/camera-motions.fr.ts +86 -0
  210. package/src/i18n/camera-motions.he.ts +86 -0
  211. package/src/i18n/camera-motions.hi.ts +99 -0
  212. package/src/i18n/camera-motions.ja.ts +99 -0
  213. package/src/i18n/camera-motions.ko.ts +99 -0
  214. package/src/i18n/camera-motions.pt-BR.ts +99 -0
  215. package/src/i18n/camera-motions.ru.ts +99 -0
  216. package/src/i18n/camera-motions.zh-CN.ts +99 -0
  217. package/src/i18n/character-fx.ar.ts +72 -0
  218. package/src/i18n/character-fx.de.ts +72 -0
  219. package/src/i18n/character-fx.es.ts +72 -0
  220. package/src/i18n/character-fx.fr.ts +72 -0
  221. package/src/i18n/character-fx.he.ts +72 -0
  222. package/src/i18n/character-fx.hi.ts +72 -0
  223. package/src/i18n/character-fx.ja.ts +72 -0
  224. package/src/i18n/character-fx.ko.ts +72 -0
  225. package/src/i18n/character-fx.pt-BR.ts +72 -0
  226. package/src/i18n/character-fx.ru.ts +72 -0
  227. package/src/i18n/character-fx.zh-CN.ts +72 -0
  228. package/src/i18n/color-look.ar.ts +50 -0
  229. package/src/i18n/color-look.de.ts +47 -0
  230. package/src/i18n/color-look.es.ts +50 -0
  231. package/src/i18n/color-look.fr.ts +50 -0
  232. package/src/i18n/color-look.he.ts +49 -0
  233. package/src/i18n/color-look.hi.ts +52 -0
  234. package/src/i18n/color-look.ja.ts +54 -0
  235. package/src/i18n/color-look.ko.ts +54 -0
  236. package/src/i18n/color-look.pt-BR.ts +54 -0
  237. package/src/i18n/color-look.ru.ts +52 -0
  238. package/src/i18n/color-look.zh-CN.ts +53 -0
  239. package/src/i18n/composition-effects.ar.ts +26 -0
  240. package/src/i18n/composition-effects.de.ts +25 -0
  241. package/src/i18n/composition-effects.es.ts +27 -0
  242. package/src/i18n/composition-effects.fr.ts +25 -0
  243. package/src/i18n/composition-effects.he.ts +25 -0
  244. package/src/i18n/composition-effects.hi.ts +27 -0
  245. package/src/i18n/composition-effects.ja.ts +27 -0
  246. package/src/i18n/composition-effects.ko.ts +27 -0
  247. package/src/i18n/composition-effects.pt-BR.ts +25 -0
  248. package/src/i18n/composition-effects.ru.ts +82 -0
  249. package/src/i18n/composition-effects.zh-CN.ts +25 -0
  250. package/src/i18n/era.ar.ts +37 -0
  251. package/src/i18n/era.de.ts +34 -0
  252. package/src/i18n/era.es.ts +37 -0
  253. package/src/i18n/era.fr.ts +37 -0
  254. package/src/i18n/era.he.ts +36 -0
  255. package/src/i18n/era.hi.ts +39 -0
  256. package/src/i18n/era.ja.ts +41 -0
  257. package/src/i18n/era.ko.ts +39 -0
  258. package/src/i18n/era.pt-BR.ts +41 -0
  259. package/src/i18n/era.ru.ts +121 -0
  260. package/src/i18n/era.zh-CN.ts +39 -0
  261. package/src/i18n/exposure-settings.ar.ts +28 -0
  262. package/src/i18n/exposure-settings.de.ts +26 -0
  263. package/src/i18n/exposure-settings.es.ts +28 -0
  264. package/src/i18n/exposure-settings.fr.ts +29 -0
  265. package/src/i18n/exposure-settings.he.ts +28 -0
  266. package/src/i18n/exposure-settings.hi.ts +31 -0
  267. package/src/i18n/exposure-settings.ja.ts +31 -0
  268. package/src/i18n/exposure-settings.ko.ts +31 -0
  269. package/src/i18n/exposure-settings.pt-BR.ts +31 -0
  270. package/src/i18n/exposure-settings.ru.ts +31 -0
  271. package/src/i18n/exposure-settings.zh-CN.ts +31 -0
  272. package/src/i18n/framing.ar.ts +78 -0
  273. package/src/i18n/framing.de.ts +73 -0
  274. package/src/i18n/framing.es.ts +78 -0
  275. package/src/i18n/framing.fr.ts +78 -0
  276. package/src/i18n/framing.he.ts +77 -0
  277. package/src/i18n/framing.hi.ts +82 -0
  278. package/src/i18n/framing.ja.ts +84 -0
  279. package/src/i18n/framing.ko.ts +82 -0
  280. package/src/i18n/framing.pt-BR.ts +84 -0
  281. package/src/i18n/framing.ru.ts +82 -0
  282. package/src/i18n/framing.zh-CN.ts +82 -0
  283. package/src/i18n/furniture.ar.ts +92 -0
  284. package/src/i18n/furniture.de.ts +93 -0
  285. package/src/i18n/furniture.es.ts +101 -0
  286. package/src/i18n/furniture.fr.ts +93 -0
  287. package/src/i18n/furniture.he.ts +101 -0
  288. package/src/i18n/furniture.hi.ts +101 -0
  289. package/src/i18n/furniture.ja.ts +101 -0
  290. package/src/i18n/furniture.ko.ts +101 -0
  291. package/src/i18n/furniture.pt-BR.ts +101 -0
  292. package/src/i18n/furniture.ru.ts +101 -0
  293. package/src/i18n/furniture.zh-CN.ts +101 -0
  294. package/src/i18n/held-prop.ar.ts +74 -0
  295. package/src/i18n/held-prop.de.ts +65 -0
  296. package/src/i18n/held-prop.es.ts +83 -0
  297. package/src/i18n/held-prop.fr.ts +74 -0
  298. package/src/i18n/held-prop.he.ts +73 -0
  299. package/src/i18n/held-prop.hi.ts +82 -0
  300. package/src/i18n/held-prop.ja.ts +84 -0
  301. package/src/i18n/held-prop.ko.ts +84 -0
  302. package/src/i18n/held-prop.pt-BR.ts +84 -0
  303. package/src/i18n/held-prop.ru.ts +82 -0
  304. package/src/i18n/held-prop.zh-CN.ts +82 -0
  305. package/src/i18n/index.ts +208 -0
  306. package/src/i18n/instrumentation.ar.ts +195 -0
  307. package/src/i18n/instrumentation.de.ts +195 -0
  308. package/src/i18n/instrumentation.es.ts +195 -0
  309. package/src/i18n/instrumentation.fr.ts +195 -0
  310. package/src/i18n/instrumentation.he.ts +195 -0
  311. package/src/i18n/instrumentation.hi.ts +195 -0
  312. package/src/i18n/instrumentation.ja.ts +195 -0
  313. package/src/i18n/instrumentation.ko.ts +195 -0
  314. package/src/i18n/instrumentation.pt-BR.ts +195 -0
  315. package/src/i18n/instrumentation.ru.ts +195 -0
  316. package/src/i18n/instrumentation.zh-CN.ts +195 -0
  317. package/src/i18n/lens.ar.ts +24 -0
  318. package/src/i18n/lens.de.ts +24 -0
  319. package/src/i18n/lens.es.ts +24 -0
  320. package/src/i18n/lens.fr.ts +24 -0
  321. package/src/i18n/lens.he.ts +24 -0
  322. package/src/i18n/lens.hi.ts +24 -0
  323. package/src/i18n/lens.ja.ts +24 -0
  324. package/src/i18n/lens.ko.ts +24 -0
  325. package/src/i18n/lens.pt-BR.ts +24 -0
  326. package/src/i18n/lens.ru.ts +25 -0
  327. package/src/i18n/lens.zh-CN.ts +24 -0
  328. package/src/i18n/lighting.ar.ts +86 -0
  329. package/src/i18n/lighting.de.ts +84 -0
  330. package/src/i18n/lighting.es.ts +89 -0
  331. package/src/i18n/lighting.fr.ts +84 -0
  332. package/src/i18n/lighting.he.ts +88 -0
  333. package/src/i18n/lighting.hi.ts +90 -0
  334. package/src/i18n/lighting.ja.ts +90 -0
  335. package/src/i18n/lighting.ko.ts +90 -0
  336. package/src/i18n/lighting.pt-BR.ts +90 -0
  337. package/src/i18n/lighting.ru.ts +88 -0
  338. package/src/i18n/lighting.zh-CN.ts +92 -0
  339. package/src/i18n/loop-subject.ar.ts +39 -0
  340. package/src/i18n/loop-subject.de.ts +39 -0
  341. package/src/i18n/loop-subject.es.ts +39 -0
  342. package/src/i18n/loop-subject.fr.ts +39 -0
  343. package/src/i18n/loop-subject.he.ts +39 -0
  344. package/src/i18n/loop-subject.hi.ts +39 -0
  345. package/src/i18n/loop-subject.ja.ts +39 -0
  346. package/src/i18n/loop-subject.ko.ts +39 -0
  347. package/src/i18n/loop-subject.pt-BR.ts +39 -0
  348. package/src/i18n/loop-subject.ru.ts +39 -0
  349. package/src/i18n/loop-subject.zh-CN.ts +39 -0
  350. package/src/i18n/materials.ar.ts +80 -0
  351. package/src/i18n/materials.de.ts +80 -0
  352. package/src/i18n/materials.es.ts +87 -0
  353. package/src/i18n/materials.fr.ts +80 -0
  354. package/src/i18n/materials.he.ts +86 -0
  355. package/src/i18n/materials.hi.ts +86 -0
  356. package/src/i18n/materials.ja.ts +88 -0
  357. package/src/i18n/materials.ko.ts +88 -0
  358. package/src/i18n/materials.pt-BR.ts +88 -0
  359. package/src/i18n/materials.ru.ts +86 -0
  360. package/src/i18n/materials.zh-CN.ts +86 -0
  361. package/src/i18n/mood.ar.ts +61 -0
  362. package/src/i18n/mood.de.ts +61 -0
  363. package/src/i18n/mood.es.ts +66 -0
  364. package/src/i18n/mood.fr.ts +61 -0
  365. package/src/i18n/mood.he.ts +64 -0
  366. package/src/i18n/mood.hi.ts +66 -0
  367. package/src/i18n/mood.ja.ts +66 -0
  368. package/src/i18n/mood.ko.ts +66 -0
  369. package/src/i18n/mood.pt-BR.ts +64 -0
  370. package/src/i18n/mood.ru.ts +64 -0
  371. package/src/i18n/mood.zh-CN.ts +64 -0
  372. package/src/i18n/music-genre.ar.ts +302 -0
  373. package/src/i18n/music-genre.de.ts +302 -0
  374. package/src/i18n/music-genre.es.ts +302 -0
  375. package/src/i18n/music-genre.fr.ts +302 -0
  376. package/src/i18n/music-genre.he.ts +302 -0
  377. package/src/i18n/music-genre.hi.ts +302 -0
  378. package/src/i18n/music-genre.ja.ts +302 -0
  379. package/src/i18n/music-genre.ko.ts +302 -0
  380. package/src/i18n/music-genre.pt-BR.ts +302 -0
  381. package/src/i18n/music-genre.ru.ts +302 -0
  382. package/src/i18n/music-genre.zh-CN.ts +302 -0
  383. package/src/i18n/music-mood.ar.ts +75 -0
  384. package/src/i18n/music-mood.de.ts +75 -0
  385. package/src/i18n/music-mood.es.ts +75 -0
  386. package/src/i18n/music-mood.fr.ts +75 -0
  387. package/src/i18n/music-mood.he.ts +75 -0
  388. package/src/i18n/music-mood.hi.ts +75 -0
  389. package/src/i18n/music-mood.ja.ts +75 -0
  390. package/src/i18n/music-mood.ko.ts +75 -0
  391. package/src/i18n/music-mood.pt-BR.ts +75 -0
  392. package/src/i18n/music-mood.ru.ts +75 -0
  393. package/src/i18n/music-mood.zh-CN.ts +75 -0
  394. package/src/i18n/person.ar.ts +638 -0
  395. package/src/i18n/person.de.ts +629 -0
  396. package/src/i18n/person.es.ts +645 -0
  397. package/src/i18n/person.fr.ts +621 -0
  398. package/src/i18n/person.he.ts +656 -0
  399. package/src/i18n/person.hi.ts +654 -0
  400. package/src/i18n/person.ja.ts +662 -0
  401. package/src/i18n/person.ko.ts +662 -0
  402. package/src/i18n/person.pt-BR.ts +656 -0
  403. package/src/i18n/person.ru.ts +656 -0
  404. package/src/i18n/person.zh-CN.ts +656 -0
  405. package/src/i18n/photo-genre.ar.ts +58 -0
  406. package/src/i18n/photo-genre.de.ts +52 -0
  407. package/src/i18n/photo-genre.es.ts +64 -0
  408. package/src/i18n/photo-genre.fr.ts +58 -0
  409. package/src/i18n/photo-genre.he.ts +62 -0
  410. package/src/i18n/photo-genre.hi.ts +63 -0
  411. package/src/i18n/photo-genre.ja.ts +65 -0
  412. package/src/i18n/photo-genre.ko.ts +63 -0
  413. package/src/i18n/photo-genre.pt-BR.ts +65 -0
  414. package/src/i18n/photo-genre.ru.ts +63 -0
  415. package/src/i18n/photo-genre.zh-CN.ts +63 -0
  416. package/src/i18n/photographer.ar.ts +75 -0
  417. package/src/i18n/photographer.de.ts +74 -0
  418. package/src/i18n/photographer.es.ts +77 -0
  419. package/src/i18n/photographer.fr.ts +77 -0
  420. package/src/i18n/photographer.he.ts +78 -0
  421. package/src/i18n/photographer.hi.ts +80 -0
  422. package/src/i18n/photographer.ja.ts +84 -0
  423. package/src/i18n/photographer.ko.ts +84 -0
  424. package/src/i18n/photographer.pt-BR.ts +83 -0
  425. package/src/i18n/photographer.ru.ts +78 -0
  426. package/src/i18n/photographer.zh-CN.ts +78 -0
  427. package/src/i18n/pose.ar.ts +95 -0
  428. package/src/i18n/pose.de.ts +96 -0
  429. package/src/i18n/pose.es.ts +104 -0
  430. package/src/i18n/pose.fr.ts +96 -0
  431. package/src/i18n/pose.he.ts +104 -0
  432. package/src/i18n/pose.hi.ts +104 -0
  433. package/src/i18n/pose.ja.ts +104 -0
  434. package/src/i18n/pose.ko.ts +104 -0
  435. package/src/i18n/pose.pt-BR.ts +104 -0
  436. package/src/i18n/pose.ru.ts +104 -0
  437. package/src/i18n/pose.zh-CN.ts +104 -0
  438. package/src/i18n/post-process-effects.ar.ts +25 -0
  439. package/src/i18n/post-process-effects.de.ts +24 -0
  440. package/src/i18n/post-process-effects.es.ts +26 -0
  441. package/src/i18n/post-process-effects.fr.ts +24 -0
  442. package/src/i18n/post-process-effects.he.ts +24 -0
  443. package/src/i18n/post-process-effects.hi.ts +26 -0
  444. package/src/i18n/post-process-effects.ja.ts +26 -0
  445. package/src/i18n/post-process-effects.ko.ts +26 -0
  446. package/src/i18n/post-process-effects.pt-BR.ts +24 -0
  447. package/src/i18n/post-process-effects.ru.ts +24 -0
  448. package/src/i18n/post-process-effects.zh-CN.ts +24 -0
  449. package/src/i18n/render-quality.ar.ts +34 -0
  450. package/src/i18n/render-quality.de.ts +30 -0
  451. package/src/i18n/render-quality.es.ts +35 -0
  452. package/src/i18n/render-quality.fr.ts +34 -0
  453. package/src/i18n/render-quality.he.ts +33 -0
  454. package/src/i18n/render-quality.hi.ts +39 -0
  455. package/src/i18n/render-quality.ja.ts +39 -0
  456. package/src/i18n/render-quality.ko.ts +39 -0
  457. package/src/i18n/render-quality.pt-BR.ts +37 -0
  458. package/src/i18n/render-quality.ru.ts +37 -0
  459. package/src/i18n/render-quality.zh-CN.ts +37 -0
  460. package/src/i18n/seasons.ar.ts +10 -0
  461. package/src/i18n/seasons.de.ts +10 -0
  462. package/src/i18n/seasons.es.ts +10 -0
  463. package/src/i18n/seasons.fr.ts +10 -0
  464. package/src/i18n/seasons.he.ts +10 -0
  465. package/src/i18n/seasons.hi.ts +10 -0
  466. package/src/i18n/seasons.ja.ts +10 -0
  467. package/src/i18n/seasons.ko.ts +10 -0
  468. package/src/i18n/seasons.pt-BR.ts +10 -0
  469. package/src/i18n/seasons.ru.ts +10 -0
  470. package/src/i18n/seasons.zh-CN.ts +10 -0
  471. package/src/i18n/setting.ar.ts +73 -0
  472. package/src/i18n/setting.de.ts +73 -0
  473. package/src/i18n/setting.es.ts +77 -0
  474. package/src/i18n/setting.fr.ts +73 -0
  475. package/src/i18n/setting.he.ts +76 -0
  476. package/src/i18n/setting.hi.ts +76 -0
  477. package/src/i18n/setting.ja.ts +78 -0
  478. package/src/i18n/setting.ko.ts +76 -0
  479. package/src/i18n/setting.pt-BR.ts +78 -0
  480. package/src/i18n/setting.ru.ts +76 -0
  481. package/src/i18n/setting.zh-CN.ts +76 -0
  482. package/src/i18n/style.ar.ts +55 -0
  483. package/src/i18n/style.de.ts +54 -0
  484. package/src/i18n/style.es.ts +54 -0
  485. package/src/i18n/style.fr.ts +54 -0
  486. package/src/i18n/style.he.ts +54 -0
  487. package/src/i18n/style.hi.ts +54 -0
  488. package/src/i18n/style.ja.ts +54 -0
  489. package/src/i18n/style.ko.ts +54 -0
  490. package/src/i18n/style.pt-BR.ts +56 -0
  491. package/src/i18n/style.ru.ts +54 -0
  492. package/src/i18n/style.zh-CN.ts +54 -0
  493. package/src/i18n/styling.ar.ts +295 -0
  494. package/src/i18n/styling.de.ts +285 -0
  495. package/src/i18n/styling.es.ts +317 -0
  496. package/src/i18n/styling.fr.ts +285 -0
  497. package/src/i18n/styling.he.ts +307 -0
  498. package/src/i18n/styling.hi.ts +323 -0
  499. package/src/i18n/styling.ja.ts +317 -0
  500. package/src/i18n/styling.ko.ts +311 -0
  501. package/src/i18n/styling.pt-BR.ts +301 -0
  502. package/src/i18n/styling.ru.ts +301 -0
  503. package/src/i18n/styling.zh-CN.ts +307 -0
  504. package/src/i18n/temporal.ar.ts +31 -0
  505. package/src/i18n/temporal.de.ts +24 -0
  506. package/src/i18n/temporal.es.ts +27 -0
  507. package/src/i18n/temporal.fr.ts +28 -0
  508. package/src/i18n/temporal.he.ts +31 -0
  509. package/src/i18n/temporal.hi.ts +31 -0
  510. package/src/i18n/temporal.ja.ts +31 -0
  511. package/src/i18n/temporal.ko.ts +31 -0
  512. package/src/i18n/temporal.pt-BR.ts +31 -0
  513. package/src/i18n/temporal.ru.ts +31 -0
  514. package/src/i18n/temporal.zh-CN.ts +31 -0
  515. package/src/i18n/transitions.ar.ts +97 -0
  516. package/src/i18n/transitions.de.ts +97 -0
  517. package/src/i18n/transitions.es.ts +97 -0
  518. package/src/i18n/transitions.fr.ts +97 -0
  519. package/src/i18n/transitions.he.ts +97 -0
  520. package/src/i18n/transitions.hi.ts +97 -0
  521. package/src/i18n/transitions.ja.ts +97 -0
  522. package/src/i18n/transitions.ko.ts +97 -0
  523. package/src/i18n/transitions.pt-BR.ts +97 -0
  524. package/src/i18n/transitions.ru.ts +97 -0
  525. package/src/i18n/transitions.zh-CN.ts +97 -0
  526. package/src/i18n/types.ts +158 -0
  527. package/src/i18n/vehicles.ar.ts +137 -0
  528. package/src/i18n/vehicles.de.ts +125 -0
  529. package/src/i18n/vehicles.es.ts +138 -0
  530. package/src/i18n/vehicles.fr.ts +125 -0
  531. package/src/i18n/vehicles.he.ts +136 -0
  532. package/src/i18n/vehicles.hi.ts +138 -0
  533. package/src/i18n/vehicles.ja.ts +138 -0
  534. package/src/i18n/vehicles.ko.ts +138 -0
  535. package/src/i18n/vehicles.pt-BR.ts +136 -0
  536. package/src/i18n/vehicles.ru.ts +136 -0
  537. package/src/i18n/vehicles.zh-CN.ts +136 -0
  538. package/src/i18n/voice-character.ar.ts +126 -0
  539. package/src/i18n/voice-character.de.ts +126 -0
  540. package/src/i18n/voice-character.es.ts +126 -0
  541. package/src/i18n/voice-character.fr.ts +126 -0
  542. package/src/i18n/voice-character.he.ts +126 -0
  543. package/src/i18n/voice-character.hi.ts +126 -0
  544. package/src/i18n/voice-character.ja.ts +126 -0
  545. package/src/i18n/voice-character.ko.ts +126 -0
  546. package/src/i18n/voice-character.pt-BR.ts +126 -0
  547. package/src/i18n/voice-character.ru.ts +126 -0
  548. package/src/i18n/voice-character.zh-CN.ts +126 -0
  549. package/src/i18n/voice-delivery.ar.ts +77 -0
  550. package/src/i18n/voice-delivery.de.ts +77 -0
  551. package/src/i18n/voice-delivery.es.ts +77 -0
  552. package/src/i18n/voice-delivery.fr.ts +77 -0
  553. package/src/i18n/voice-delivery.he.ts +77 -0
  554. package/src/i18n/voice-delivery.hi.ts +77 -0
  555. package/src/i18n/voice-delivery.ja.ts +77 -0
  556. package/src/i18n/voice-delivery.ko.ts +77 -0
  557. package/src/i18n/voice-delivery.pt-BR.ts +77 -0
  558. package/src/i18n/voice-delivery.ru.ts +77 -0
  559. package/src/i18n/voice-delivery.zh-CN.ts +77 -0
  560. package/src/i18n/weapons.ar.ts +115 -0
  561. package/src/i18n/weapons.de.ts +114 -0
  562. package/src/i18n/weapons.es.ts +116 -0
  563. package/src/i18n/weapons.fr.ts +103 -0
  564. package/src/i18n/weapons.he.ts +114 -0
  565. package/src/i18n/weapons.hi.ts +116 -0
  566. package/src/i18n/weapons.ja.ts +116 -0
  567. package/src/i18n/weapons.ko.ts +116 -0
  568. package/src/i18n/weapons.pt-BR.ts +114 -0
  569. package/src/i18n/weapons.ru.ts +114 -0
  570. package/src/i18n/weapons.zh-CN.ts +114 -0
  571. package/src/image-critic-node.ts +61 -0
  572. package/src/image-critic-types.ts +40 -0
  573. package/src/index.ts +866 -0
  574. package/src/inject-upstream.ts +15 -0
  575. package/src/inpaint-region.ts +51 -0
  576. package/src/json-evaluator.ts +732 -0
  577. package/src/json-path.ts +52 -0
  578. package/src/lip-sync-pricing.ts +90 -0
  579. package/src/list-merge.ts +40 -0
  580. package/src/list-sort.ts +122 -0
  581. package/src/list-source-resolver.ts +50 -0
  582. package/src/llm-models.ts +245 -0
  583. package/src/location-mention-slug.ts +291 -0
  584. package/src/location-preset-catalog-map.ts +128 -0
  585. package/src/loop-delimiter.ts +73 -0
  586. package/src/lora-routing.ts +95 -0
  587. package/src/lottie-overlay-catalog.ts +175 -0
  588. package/src/lottie-slots.ts +309 -0
  589. package/src/model-catalog.ts +2386 -0
  590. package/src/model-constants.ts +1910 -0
  591. package/src/model-tree.ts +120 -0
  592. package/src/monetization.ts +15 -0
  593. package/src/multi-pick.ts +42 -0
  594. package/src/node-default-mappings.ts +262 -0
  595. package/src/node-mappable-fields.ts +70 -0
  596. package/src/node-preset-export.ts +56 -0
  597. package/src/node-preset-extract.ts +147 -0
  598. package/src/node-refs.ts +253 -0
  599. package/src/node-runtime-keys.ts +117 -0
  600. package/src/object-aspect-defaults.ts +72 -0
  601. package/src/object-picker-types.ts +22 -0
  602. package/src/parameter-node-value.ts +284 -0
  603. package/src/pinterest-url.ts +28 -0
  604. package/src/pipeline-chat.ts +72 -0
  605. package/src/pipeline-defaults.ts +318 -0
  606. package/src/pipeline-events.ts +162 -0
  607. package/src/pipeline-state-types.ts +196 -0
  608. package/src/pipeline-types.ts +647 -0
  609. package/src/pipeline-validation.ts +87 -0
  610. package/src/presentation-types.ts +23 -0
  611. package/src/presentation-utils.ts +397 -0
  612. package/src/producer-types.ts +188 -0
  613. package/src/progress-curve.ts +99 -0
  614. package/src/provider-directive-defaults.ts +69 -0
  615. package/src/reduce-strategy-registry.ts +121 -0
  616. package/src/reference-board-templates.ts +81 -0
  617. package/src/reference-roles.ts +153 -0
  618. package/src/reference-sheet/__tests__/board-to-column.test.ts +20 -0
  619. package/src/reference-sheet/__tests__/catalog.test.ts +50 -0
  620. package/src/reference-sheet/__tests__/cost.test.ts +38 -0
  621. package/src/reference-sheet/__tests__/no-drift.test.ts +28 -0
  622. package/src/reference-sheet/__tests__/panel-plan.test.ts +61 -0
  623. package/src/reference-sheet/__tests__/panel-prompts.test.ts +17 -0
  624. package/src/reference-sheet/__tests__/plan-generation.test.ts +72 -0
  625. package/src/reference-sheet/__tests__/presets.test.ts +54 -0
  626. package/src/reference-sheet/__tests__/types.test.ts +24 -0
  627. package/src/reference-sheet/catalog.ts +144 -0
  628. package/src/reference-sheet/cost.ts +37 -0
  629. package/src/reference-sheet/credit-id.ts +7 -0
  630. package/src/reference-sheet/index.ts +8 -0
  631. package/src/reference-sheet/panel-plan.ts +65 -0
  632. package/src/reference-sheet/panel-prompts.ts +14 -0
  633. package/src/reference-sheet/plan-generation.ts +79 -0
  634. package/src/reference-sheet/presets.ts +77 -0
  635. package/src/reference-sheet/types.ts +92 -0
  636. package/src/repeat-types.ts +97 -0
  637. package/src/resolve-field-mappings.ts +119 -0
  638. package/src/route-filter.ts +80 -0
  639. package/src/scene-helper-types.ts +249 -0
  640. package/src/scene-node-types.ts +281 -0
  641. package/src/scraper-actors.ts +57 -0
  642. package/src/scraper-output-schemas.ts +25 -0
  643. package/src/seasons.ts +45 -0
  644. package/src/selector.ts +864 -0
  645. package/src/settled-with-limit.ts +42 -0
  646. package/src/shot-element.ts +43 -0
  647. package/src/social-media-specs.ts +121 -0
  648. package/src/social-post.ts +24 -0
  649. package/src/sub-workflow-validation.ts +78 -0
  650. package/src/supported-fonts.ts +40 -0
  651. package/src/surround.ts +113 -0
  652. package/src/switchx-pricing.ts +33 -0
  653. package/src/text-separators.ts +59 -0
  654. package/src/to-connected-references.ts +77 -0
  655. package/src/types.ts +272 -0
  656. package/src/variant-job-id.ts +17 -0
  657. package/src/vehicles.ts +213 -0
  658. package/src/video-analysis-pricing.ts +74 -0
  659. package/src/video-analysis.ts +127 -0
  660. package/src/video-duration.ts +25 -0
  661. package/src/voice-changer-models.ts +36 -0
  662. package/src/voice-matcher-types.ts +37 -0
  663. package/src/voices.ts +92 -0
  664. package/src/weapons.ts +190 -0
  665. package/src/workflow-export.ts +153 -0
@@ -0,0 +1,2386 @@
1
+ /**
2
+ * Model catalog — single source of truth for what each Nodaro-exposed model
3
+ * is, who made it, what it's good for, and which knobs it accepts.
4
+ *
5
+ * Consumed by:
6
+ * - Backend MCP `list_models` tool (renders the nested per-kind / per-family
7
+ * response so Claude can pick the right model for a user's intent)
8
+ * - (future) Frontend config panels — today they read parallel registries
9
+ * in `frontend/src/components/editor/config-panels/model-options.ts`; a
10
+ * follow-up will migrate them to import from here.
11
+ *
12
+ * Authoring rules:
13
+ * - Keep `description` to one line (≤120 chars). It's what Claude reads to
14
+ * decide which model to use.
15
+ * - `useCases` is a small set of short tags (1-3 words) — verbs/nouns Claude
16
+ * can match against user intent ("typography", "character", "fast",
17
+ * "cinematic"). Stay consistent across entries: don't write "text" in one
18
+ * and "typography" in another.
19
+ * - `aspectRatios` / `resolutions` / `qualities` only appear when the model
20
+ * actually exposes that lever (e.g., Nano Banana base has no `resolution`).
21
+ * - `durations` is in seconds.
22
+ * - `pricing[0]` is ALWAYS the default variant (the bare id without a
23
+ * composite suffix like `:4K` or `:high`).
24
+ * - Costs are sourced from `STATIC_CREDIT_COSTS` (see backend
25
+ * `src/billing/credits.ts`). Keep them in sync if the table changes.
26
+ * - After editing `MODEL_CATALOG` or `MODEL_RECOMMENDATIONS`, run
27
+ * `npm run gen:skills` (from `backend/`) to refresh the public
28
+ * `docs/choosing-models.md` guide. CI (`gen:skills:check`) fails on drift.
29
+ */
30
+
31
+ export type ModelKind = "image" | "video" | "audio"
32
+
33
+ export type ModelMode =
34
+ // image
35
+ | "t2i"
36
+ | "i2i"
37
+ | "edit"
38
+ | "upscale"
39
+ | "remove-bg"
40
+ // video
41
+ | "i2v"
42
+ | "t2v"
43
+ | "v2v"
44
+ | "extend"
45
+ | "motion-transfer"
46
+ | "lip-sync"
47
+ | "video-upscale"
48
+ // audio
49
+ | "tts"
50
+ | "music"
51
+ | "sfx"
52
+ | "stt"
53
+ | "voice-clone"
54
+ | "voice-design"
55
+ | "voice-changer"
56
+ | "isolation"
57
+ | "dubbing"
58
+ | "forced-alignment"
59
+ // video analysis
60
+ | "video-analysis"
61
+
62
+ export interface PriceVariant {
63
+ /** Composite identifier as it appears in `STATIC_CREDIT_COSTS`. */
64
+ identifier: string
65
+ /** Credits charged at reservation. */
66
+ credits: number
67
+ /** Short human-readable note describing this variant ("1K default", "4K", "with audio"). */
68
+ note?: string
69
+ }
70
+
71
+ export interface ModelCatalogEntry {
72
+ id: string
73
+ kind: ModelKind
74
+ /**
75
+ * Operations this model supports. Many video models (minimax, veo3,
76
+ * seedance) support BOTH i2v AND t2v under the same id — the route handler
77
+ * picks which KIE endpoint to hit based on whether an image was supplied.
78
+ * For those, list both modes here so MCP filters and frontend pickers see
79
+ * the model in either context.
80
+ */
81
+ modes: readonly ModelMode[]
82
+ /** Vendor / lab that produced the model. Used for UI grouping. */
83
+ family: string
84
+ /** Display name (NOT the id — the id is the API enum value). */
85
+ label: string
86
+ /**
87
+ * Model LINE for the add-node Models tab — the brand-agnostic product line a
88
+ * user thinks in ("Nano Banana", "Flux", "VEO", "Kling", "ElevenLabs").
89
+ * Distinct from `family` (the lab: Google covers Nano Banana + Imagen + VEO).
90
+ * REQUIRED for every node-creatable model; a completeness guard in
91
+ * `@nodaro/shared` (model-tree.test.ts) fails if one is missing.
92
+ */
93
+ series?: string
94
+ /**
95
+ * The value written into the creating node's data field when this model is
96
+ * picked from the Models tab, for models whose catalog id differs from the
97
+ * stored value. Only `suno-generate` diverges today (id `suno-v5` → `data.model`
98
+ * "V5"). Omit when the id IS the stored value (the common case).
99
+ */
100
+ dataValue?: string
101
+ /** One-liner. Visible to Claude in MCP `list_models` output. */
102
+ description: string
103
+ /** Short tags Claude matches against user intent. */
104
+ useCases: readonly string[]
105
+ /** Capabilities flags: "reference-image", "end-frame", "audio", etc. */
106
+ features?: readonly string[]
107
+ aspectRatios?: readonly string[]
108
+ resolutions?: readonly string[]
109
+ qualities?: readonly string[]
110
+ durations?: readonly number[]
111
+ pricing: readonly PriceVariant[]
112
+ /** Editorial highlight — "best in tier". Surfaces in MCP output as a ⭐. */
113
+ featured?: boolean
114
+ /**
115
+ * Per-model overrides for the global value label decorations (e.g. Hailuo
116
+ * 2.3 Pro labels its 1080P resolution as "1080P (6s max)" because the
117
+ * model caps long durations at lower resolution). Falls back to
118
+ * `MODEL_VALUE_LABELS` for values not listed here.
119
+ */
120
+ valueLabels?: Record<string, string>
121
+ /**
122
+ * Hide from MCP `list_models` output and tool model enums. Used for legacy
123
+ * model versions superseded by newer entries — they remain in the catalog
124
+ * (and accessible via direct API / frontend pickers for back-compat) but
125
+ * stop showing up in Claude.ai's model picker, which keeps the surface
126
+ * tight and steers the agent to the current generation.
127
+ *
128
+ * Frontend pickers ignore this flag.
129
+ */
130
+ mcpHidden?: boolean
131
+ }
132
+
133
+ /**
134
+ * Editorial recommendations — short "best for X" picks Claude can echo back
135
+ * to the user when they don't know which model to use. Surfaces as a "Quick
136
+ * recommendations" footer in MCP `list_models` output.
137
+ */
138
+ export interface ModelRecommendation {
139
+ intent: string
140
+ modelIds: readonly string[]
141
+ note: string
142
+ }
143
+
144
+ export const MODEL_RECOMMENDATIONS: readonly ModelRecommendation[] = [
145
+ // image
146
+ { intent: "best for typography / logos / text-heavy", modelIds: ["nano-banana-pro", "gpt-image-2"], note: "Nano Banana Pro for diagrams / complex text; GPT Image 2 for logos and short copy." },
147
+ { intent: "cheapest realistic image", modelIds: ["z-image", "qwen", "imagen4-fast"], note: "Z-Image is the cheapest at 1 credit. Qwen / Imagen4 Fast for slightly higher quality." },
148
+ { intent: "highest fidelity image", modelIds: ["nano-banana-pro", "imagen4-ultra", "flux-flex"], note: "Pick by family preference; all three are premium tiers." },
149
+ { intent: "image edit / restyle", modelIds: ["flux-kontext", "ideogram-remix", "seedream-5-lite-i2i"], note: "Flux Kontext preserves identity; Ideogram Remix is character-aware; Seedream 5 Lite for instruction-based edits." },
150
+ { intent: "highest-resolution image (4K / 8K)", modelIds: ["topaz-image-upscale", "nano-banana-pro", "gpt-image-2"], note: "Generate at native then Topaz upscale for 8K." },
151
+ { intent: "background removal / cutout", modelIds: ["recraft-remove-bg"], note: "1 credit, no prompt needed." },
152
+ // video
153
+ { intent: "best cinematic video", modelIds: ["veo3", "kling-3.0", "seedance-2"], note: "VEO 3.1 Quality for premium narrative; Kling 3.0 for music-synced motion; Seedance 2 for reference-driven consistency." },
154
+ { intent: "cheap batch video clips", modelIds: ["veo3.1", "wan-turbo", "bytedance-lite"], note: "VEO 3.1 Fast is the best price/quality balance with native audio." },
155
+ { intent: "video with start + end frame", modelIds: ["veo3", "veo3.1", "kling-turbo", "minimax", "hailuo-standard", "seedance-2"], note: "All listed support an end frame; VEO uses imageUrls[start, end]." },
156
+ { intent: "music / song generation", modelIds: ["suno-v5", "suno"], note: "Suno v5 has better vocal quality at the same price." },
157
+ { intent: "voice over / narration", modelIds: ["elevenlabs-v3", "elevenlabs-turbo"], note: "v3 supports [audio tags] for emotion; Turbo is cheaper for plain narration." },
158
+ { intent: "lip-sync a portrait to audio", modelIds: ["kling-avatar-pro", "kling-avatar", "infinitalk"], note: "Pro for best mouth shape; InfiniTalk for resolution control." },
159
+ { intent: "transcription / captions", modelIds: ["elevenlabs-stt"], note: "Word-level timestamps available." },
160
+ { intent: "motion transfer (drive a subject by another video)", modelIds: ["motion-transfer", "kling-3.0-motion"], note: "Kling 2.6 base is cheap; Kling 3.0 is premium." },
161
+ ] as const
162
+
163
+ // =============================================================================
164
+ // Aspect ratio presets (mirrored from frontend's model-options.ts so backend
165
+ // can consume them. Keep in sync when adding a new ratio set.)
166
+ // =============================================================================
167
+ const NANO_BANANA_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9"] as const
168
+ // Note: these arrays are synchronized with the frontend's
169
+ // `model-options.ts` constants of the same names. The frontend now imports
170
+ // them via the `getAspectRatioOptions(modelId)` helper; if you update one
171
+ // you no longer need to touch the other.
172
+ const FLUX_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"] as const
173
+ const KONTEXT_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "21:9"] as const
174
+ const GROK_RATIOS = ["1:1", "16:9", "9:16", "3:2", "2:3"] as const
175
+ const GPT_IMAGE_RATIOS = ["1:1", "3:2", "2:3"] as const
176
+ const GPT_IMAGE_2_RATIOS = ["auto", "1:1", "16:9", "9:16", "4:3", "3:4"] as const
177
+ const IMAGEN4_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4"] as const
178
+ const IDEOGRAM_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4"] as const
179
+ const SEEDREAM_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "21:9"] as const
180
+ const Z_IMAGE_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4"] as const
181
+
182
+ const VIDEO_RATIOS_HV = ["16:9", "9:16"] as const
183
+ const VIDEO_RATIOS_HVS = ["16:9", "9:16", "1:1"] as const
184
+ const WAN_27_IMAGE_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "21:9", "8:1", "1:8"] as const
185
+ const VIDEO_RATIOS_HVS345 = ["16:9", "9:16", "1:1", "4:3", "3:4"] as const
186
+ // Seedance 2.x only — KIE accepts the full fixed-ratio set incl. 21:9
187
+ // (docs.kie.ai/market/bytedance/seedance-2). Kept separate from HVS so the
188
+ // wider set can't leak to models that don't support it.
189
+ const VIDEO_RATIOS_SEEDANCE_2 = ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "adaptive"] as const
190
+
191
+ // =============================================================================
192
+ // IMAGE MODELS
193
+ // =============================================================================
194
+ const IMAGE_MODELS: Record<string, ModelCatalogEntry> = {
195
+ // ── Google Nano Banana ──
196
+ "nano-banana": {
197
+ id: "nano-banana",
198
+ kind: "image",
199
+ // Accepts a reference image via image_input — works as both t2i (no
200
+ // ref) and i2i (with ref) under the same id, so list in both modes.
201
+ modes: ["t2i", "i2i"] as const,
202
+ family: "Google",
203
+ label: "Nano Banana",
204
+ series: "Nano Banana",
205
+ description: "Budget-friendly realistic generation. Wide aspect-ratio support up to 21:9.",
206
+ useCases: ["realistic", "general", "fast"],
207
+ features: ["reference-image"],
208
+ aspectRatios: NANO_BANANA_RATIOS,
209
+ pricing: [{ identifier: "nano-banana", credits: 1, note: "1K" }],
210
+ mcpHidden: true,
211
+ },
212
+ "nano-banana-2": {
213
+ id: "nano-banana-2",
214
+ kind: "image",
215
+ // Accepts reference images via image_input — same as the rest of the
216
+ // Nano Banana family — so reachable for both t2i and i2i.
217
+ modes: ["t2i", "i2i"] as const,
218
+ family: "Google",
219
+ label: "Nano Banana 2",
220
+ series: "Nano Banana",
221
+ description: "Newer Nano Banana with native resolution control (1K/2K/4K) and Google Search context.",
222
+ useCases: ["realistic", "high-res", "factual"],
223
+ features: ["reference-image", "google-search"],
224
+ aspectRatios: NANO_BANANA_RATIOS,
225
+ resolutions: ["1K", "2K", "4K"],
226
+ pricing: [
227
+ { identifier: "nano-banana-2", credits: 4, note: "1K default" },
228
+ { identifier: "nano-banana-2:2K", credits: 5, note: "2K" },
229
+ { identifier: "nano-banana-2:4K", credits: 5, note: "4K" },
230
+ ],
231
+ },
232
+ "nano-banana-pro": {
233
+ id: "nano-banana-pro",
234
+ kind: "image",
235
+ modes: ["t2i", "i2i"] as const,
236
+ family: "Google",
237
+ label: "Nano Banana Pro",
238
+ series: "Nano Banana",
239
+ description: "Top-tier Nano Banana — best for text rendering, diagrams, and complex compositions.",
240
+ useCases: ["typography", "diagrams", "premium", "text-heavy"],
241
+ features: ["reference-image"],
242
+ aspectRatios: NANO_BANANA_RATIOS,
243
+ resolutions: ["1K", "2K", "4K"],
244
+ pricing: [
245
+ { identifier: "nano-banana-pro", credits: 5, note: "1K / 2K" },
246
+ { identifier: "nano-banana-pro:4K", credits: 6, note: "4K" },
247
+ ],
248
+ featured: true,
249
+ },
250
+ "nano-banana-edit": {
251
+ id: "nano-banana-edit",
252
+ kind: "image",
253
+ modes: ["edit"] as const,
254
+ family: "Google",
255
+ label: "Nano Banana Edit",
256
+ series: "Nano Banana",
257
+ description: "Image-to-image edits via Google's Nano Banana family. Good general-purpose editor.",
258
+ useCases: ["edit", "remix", "general"],
259
+ features: ["reference-image"],
260
+ aspectRatios: NANO_BANANA_RATIOS,
261
+ pricing: [{ identifier: "nano-banana-edit", credits: 2 }],
262
+ },
263
+
264
+ // ── Black Forest Labs (Flux) ──
265
+ "flux": {
266
+ id: "flux",
267
+ kind: "image",
268
+ modes: ["t2i"] as const,
269
+ family: "Black Forest Labs",
270
+ label: "Flux 2 Pro",
271
+ series: "Flux",
272
+ description: "Flux 2 Pro text-to-image. Strong realism, fast. Resolution lever to 2K.",
273
+ useCases: ["realistic", "general"],
274
+ features: ["reference-image"],
275
+ aspectRatios: FLUX_RATIOS,
276
+ resolutions: ["1K", "2K"],
277
+ pricing: [
278
+ { identifier: "flux", credits: 2, note: "1K default" },
279
+ { identifier: "flux:2K", credits: 2, note: "2K" },
280
+ ],
281
+ },
282
+ "flux-flex": {
283
+ id: "flux-flex",
284
+ kind: "image",
285
+ modes: ["t2i"] as const,
286
+ family: "Black Forest Labs",
287
+ label: "Flux 2 Flex",
288
+ series: "Flux",
289
+ description: "Flux 2 Flex — premium fidelity, more flexible composition. Pricier than Pro.",
290
+ useCases: ["premium", "fidelity", "realistic"],
291
+ features: ["reference-image"],
292
+ aspectRatios: FLUX_RATIOS,
293
+ resolutions: ["1K", "2K"],
294
+ pricing: [
295
+ { identifier: "flux-flex", credits: 4, note: "1K default" },
296
+ { identifier: "flux-flex:2K", credits: 6, note: "2K" },
297
+ ],
298
+ },
299
+ "flux-i2i": {
300
+ id: "flux-i2i",
301
+ kind: "image",
302
+ modes: ["i2i"] as const,
303
+ family: "Black Forest Labs",
304
+ label: "Flux 2 Flex (I2I)",
305
+ series: "Flux",
306
+ description: "Image-to-image with Flux Flex. Honors source image structure while applying prompt.",
307
+ useCases: ["edit", "restyle", "transform"],
308
+ features: ["reference-image"],
309
+ aspectRatios: FLUX_RATIOS,
310
+ resolutions: ["1K", "2K"],
311
+ pricing: [
312
+ { identifier: "flux-i2i", credits: 4, note: "1K default" },
313
+ { identifier: "flux-i2i:2K", credits: 6, note: "2K" },
314
+ ],
315
+ },
316
+ "flux-pro-i2i": {
317
+ id: "flux-pro-i2i",
318
+ kind: "image",
319
+ modes: ["i2i"] as const,
320
+ family: "Black Forest Labs",
321
+ label: "Flux 2 Pro (I2I)",
322
+ series: "Flux",
323
+ description: "Image-to-image with Flux Pro. Cheaper than Flex variant, good general edits.",
324
+ useCases: ["edit", "restyle"],
325
+ features: ["reference-image"],
326
+ aspectRatios: FLUX_RATIOS,
327
+ resolutions: ["1K", "2K"],
328
+ pricing: [
329
+ { identifier: "flux-pro-i2i", credits: 2, note: "1K default" },
330
+ { identifier: "flux-pro-i2i:2K", credits: 2, note: "2K" },
331
+ ],
332
+ },
333
+ "flux-kontext": {
334
+ id: "flux-kontext",
335
+ kind: "image",
336
+ // Kontext supports both pure t2i (no input image) and edit (with input).
337
+ modes: ["t2i", "edit"] as const,
338
+ family: "Black Forest Labs",
339
+ label: "Flux Kontext Pro",
340
+ series: "Flux",
341
+ description: "Context-aware editing and style transfer. Strong at preserving subject identity through edits.",
342
+ useCases: ["edit", "style-transfer", "preserve-identity"],
343
+ features: ["reference-image"],
344
+ aspectRatios: KONTEXT_RATIOS,
345
+ pricing: [{ identifier: "flux-kontext", credits: 2 }],
346
+ },
347
+ "flux-kontext-max": {
348
+ id: "flux-kontext-max",
349
+ kind: "image",
350
+ modes: ["t2i", "edit"] as const,
351
+ family: "Black Forest Labs",
352
+ label: "Flux Kontext Max",
353
+ series: "Flux",
354
+ description: "Premium Kontext — highest fidelity context-aware edits.",
355
+ useCases: ["edit", "style-transfer", "premium"],
356
+ features: ["reference-image"],
357
+ aspectRatios: KONTEXT_RATIOS,
358
+ pricing: [{ identifier: "flux-kontext-max", credits: 4 }],
359
+ },
360
+ // ── Replicate "Open" (uncensored) — run direct via Replicate, not KIE ──
361
+ "flux-2-klein": {
362
+ id: "flux-2-klein",
363
+ kind: "image",
364
+ modes: ["t2i"] as const,
365
+ family: "Black Forest Labs",
366
+ label: "Flux 2 Klein (Open)",
367
+ series: "Flux",
368
+ description: "Open Flux 2 9B from BFL — fast, no safety filter. Runs direct on Replicate.",
369
+ useCases: ["realistic", "fast", "unfiltered"],
370
+ features: ["reference-image"],
371
+ aspectRatios: FLUX_RATIOS,
372
+ resolutions: ["0.5 MP", "1 MP", "2 MP", "4 MP"],
373
+ pricing: [
374
+ { identifier: "flux-2-klein", credits: 1 }, // 1MP 0ref (default)
375
+ { identifier: "flux-2-klein:1MP:0ref", credits: 1 }, // min
376
+ { identifier: "flux-2-klein:4MP:8ref", credits: 11 }, // max
377
+ ],
378
+ },
379
+ "kontext-multi": {
380
+ id: "kontext-multi",
381
+ kind: "image",
382
+ modes: ["i2i", "edit"] as const,
383
+ family: "Black Forest Labs",
384
+ label: "Kontext Multi (Open)",
385
+ series: "Flux",
386
+ description: "Multi-image Flux Kontext Pro via Replicate — up to 4 input images, no safety filter.",
387
+ useCases: ["edit", "multi-reference", "unfiltered"],
388
+ features: ["reference-image"],
389
+ aspectRatios: KONTEXT_RATIOS,
390
+ pricing: [{ identifier: "kontext-multi", credits: 3 }],
391
+ },
392
+ "flux-2-pro": {
393
+ id: "flux-2-pro",
394
+ kind: "image",
395
+ modes: ["t2i", "i2i"] as const,
396
+ family: "Black Forest Labs",
397
+ label: "Flux 2 Pro (Safety Tolerance)",
398
+ series: "Flux",
399
+ description: "BFL Flux 2 Pro flagship via Replicate — exposes the `safety_tolerance` lever pinned to 5 (max for Pro). Accepts up to 4 reference images.",
400
+ useCases: ["realistic", "high-quality", "unfiltered", "reference"],
401
+ features: ["reference-image"],
402
+ aspectRatios: FLUX_RATIOS,
403
+ resolutions: ["0.5 MP", "1 MP", "2 MP", "4 MP"],
404
+ pricing: [
405
+ { identifier: "flux-2-pro", credits: 3 }, // 2MP 0ref (default)
406
+ { identifier: "flux-2-pro:0.5MP:0ref", credits: 2 }, // min
407
+ { identifier: "flux-2-pro:4MP:8ref", credits: 28 }, // max
408
+ ],
409
+ },
410
+ "flux-2-max": {
411
+ id: "flux-2-max",
412
+ kind: "image",
413
+ modes: ["t2i", "i2i"] as const,
414
+ family: "Black Forest Labs",
415
+ label: "Flux 2 Max (Safety Tolerance)",
416
+ series: "Flux",
417
+ description: "BFL Flux 2 Max — even larger sibling of Pro via Replicate, safety_tolerance=5, up to 8 reference images. Variable pricing by MP and ref count.",
418
+ useCases: ["realistic", "high-quality", "unfiltered", "multi-reference"],
419
+ features: ["reference-image"],
420
+ aspectRatios: FLUX_RATIOS,
421
+ resolutions: ["0.5 MP", "1 MP", "2 MP", "4 MP"],
422
+ pricing: [
423
+ { identifier: "flux-2-max", credits: 7 }, // 2MP 0ref (default)
424
+ { identifier: "flux-2-max:0.5MP:0ref", credits: 2 }, // min
425
+ { identifier: "flux-2-max:4MP:8ref", credits: 62 }, // max
426
+ ],
427
+ },
428
+
429
+ // ── OpenAI (GPT Image) ──
430
+ "gpt-image": {
431
+ id: "gpt-image",
432
+ kind: "image",
433
+ modes: ["t2i"] as const,
434
+ family: "OpenAI",
435
+ label: "GPT Image 1.5",
436
+ series: "GPT Image",
437
+ description: "Best for text rendering, typography, logos, and graphic design. Limited aspect ratios.",
438
+ useCases: ["typography", "logo", "graphic-design", "text-heavy"],
439
+ features: ["reference-image"],
440
+ aspectRatios: GPT_IMAGE_RATIOS,
441
+ qualities: ["medium", "high"],
442
+ pricing: [
443
+ { identifier: "gpt-image", credits: 4, note: "medium default" },
444
+ { identifier: "gpt-image:high", credits: 6, note: "high quality" },
445
+ ],
446
+ valueLabels: { "3:2": "3:2 (Landscape)", "2:3": "2:3 (Portrait)" },
447
+ mcpHidden: true,
448
+ },
449
+ "gpt-image-i2i": {
450
+ id: "gpt-image-i2i",
451
+ kind: "image",
452
+ modes: ["i2i"] as const,
453
+ family: "OpenAI",
454
+ label: "GPT Image 1.5 (I2I)",
455
+ series: "GPT Image",
456
+ description: "Image-to-image with GPT Image 1.5. Good text-aware edits.",
457
+ useCases: ["edit", "typography", "remix"],
458
+ features: ["reference-image"],
459
+ aspectRatios: GPT_IMAGE_RATIOS,
460
+ qualities: ["medium", "high"],
461
+ pricing: [
462
+ { identifier: "gpt-image-i2i", credits: 4, note: "medium default" },
463
+ { identifier: "gpt-image-i2i:high", credits: 6, note: "high quality" },
464
+ ],
465
+ valueLabels: { "3:2": "3:2 (Landscape)", "2:3": "2:3 (Portrait)" },
466
+ mcpHidden: true,
467
+ },
468
+ "gpt-image-2": {
469
+ id: "gpt-image-2",
470
+ kind: "image",
471
+ modes: ["t2i"] as const,
472
+ family: "OpenAI",
473
+ label: "GPT Image 2",
474
+ series: "GPT Image",
475
+ description: "Next-gen GPT Image — broader aspect ratios, resolution-based pricing (1K/2K/4K).",
476
+ useCases: ["typography", "high-res", "general"],
477
+ features: ["reference-image"],
478
+ aspectRatios: GPT_IMAGE_2_RATIOS,
479
+ resolutions: ["1K", "2K", "4K"],
480
+ pricing: [
481
+ { identifier: "gpt-image-2", credits: 1, note: "1K default" },
482
+ { identifier: "gpt-image-2:2K", credits: 3, note: "2K" },
483
+ { identifier: "gpt-image-2:4K", credits: 6, note: "4K" },
484
+ ],
485
+ },
486
+ "gpt-image-2-i2i": {
487
+ id: "gpt-image-2-i2i",
488
+ kind: "image",
489
+ modes: ["i2i"] as const,
490
+ family: "OpenAI",
491
+ label: "GPT Image 2 (I2I)",
492
+ series: "GPT Image",
493
+ description: "Image-to-image with GPT Image 2.",
494
+ useCases: ["edit", "high-res"],
495
+ features: ["reference-image"],
496
+ aspectRatios: GPT_IMAGE_2_RATIOS,
497
+ resolutions: ["1K", "2K", "4K"],
498
+ pricing: [
499
+ { identifier: "gpt-image-2-i2i", credits: 1, note: "1K default" },
500
+ { identifier: "gpt-image-2-i2i:2K", credits: 3, note: "2K" },
501
+ { identifier: "gpt-image-2-i2i:4K", credits: 6, note: "4K" },
502
+ ],
503
+ },
504
+
505
+ // ── Ideogram ──
506
+ // Ideogram models use a `rendering_speed` (TURBO / BALANCED / QUALITY)
507
+ // dimension that's distinct from the `quality` lever — frontend exposes
508
+ // it via a separate dropdown — so we don't model it under `qualities`.
509
+ // The composite pricing identifiers (`ideogram-v3:TURBO`, etc.) capture
510
+ // the per-tier credit cost without claiming Claude can pick it from
511
+ // the MCP tool's `quality` enum.
512
+ "ideogram-v3": {
513
+ id: "ideogram-v3",
514
+ kind: "image",
515
+ modes: ["t2i"] as const,
516
+ family: "Ideogram",
517
+ label: "Ideogram V3",
518
+ series: "Ideogram",
519
+ description: "Strong typography and stylized illustration. Speed/quality tiered (TURBO/BALANCED/QUALITY).",
520
+ useCases: ["typography", "illustration", "stylized"],
521
+ aspectRatios: IDEOGRAM_RATIOS,
522
+ pricing: [
523
+ { identifier: "ideogram-v3", credits: 2, note: "BALANCED default" },
524
+ { identifier: "ideogram-v3:TURBO", credits: 1, note: "fastest" },
525
+ { identifier: "ideogram-v3:QUALITY", credits: 3, note: "best quality" },
526
+ ],
527
+ },
528
+ "ideogram-edit": {
529
+ id: "ideogram-edit",
530
+ kind: "image",
531
+ modes: ["edit"] as const,
532
+ family: "Ideogram",
533
+ label: "Ideogram Edit",
534
+ series: "Ideogram",
535
+ description: "Inpainting / mask-based editing with Ideogram. Pair with a mask URL.",
536
+ useCases: ["edit", "inpaint", "typography"],
537
+ features: ["reference-image", "mask"],
538
+ // No aspectRatios — Ideogram Edit takes output dimensions from the input
539
+ // image + mask. Backend strips aspect_ratio for this provider.
540
+ pricing: [
541
+ { identifier: "ideogram-edit", credits: 5, note: "BALANCED default" },
542
+ { identifier: "ideogram-edit:TURBO", credits: 3, note: "fastest" },
543
+ { identifier: "ideogram-edit:QUALITY", credits: 6, note: "best quality" },
544
+ ],
545
+ },
546
+ "ideogram-remix": {
547
+ id: "ideogram-remix",
548
+ kind: "image",
549
+ modes: ["i2i"] as const,
550
+ family: "Ideogram",
551
+ label: "Ideogram Remix",
552
+ series: "Ideogram",
553
+ description: "Ideogram remix — character-aware restyling driven by reference images.",
554
+ useCases: ["remix", "character", "stylized"],
555
+ features: ["reference-image"],
556
+ aspectRatios: IDEOGRAM_RATIOS,
557
+ pricing: [
558
+ { identifier: "ideogram-remix", credits: 5, note: "BALANCED default" },
559
+ { identifier: "ideogram-remix:TURBO", credits: 3, note: "fastest" },
560
+ { identifier: "ideogram-remix:QUALITY", credits: 6, note: "best quality" },
561
+ ],
562
+ },
563
+ "ideogram-reframe": {
564
+ id: "ideogram-reframe",
565
+ kind: "image",
566
+ modes: ["edit"] as const,
567
+ family: "Ideogram",
568
+ label: "Ideogram Reframe",
569
+ series: "Ideogram",
570
+ description: "Outpaint / reframe to a new aspect ratio while preserving subject.",
571
+ useCases: ["outpaint", "reframe"],
572
+ features: ["reference-image"],
573
+ aspectRatios: IDEOGRAM_RATIOS,
574
+ pricing: [
575
+ { identifier: "ideogram-reframe", credits: 2, note: "BALANCED default" },
576
+ { identifier: "ideogram-reframe:TURBO", credits: 1, note: "fastest" },
577
+ { identifier: "ideogram-reframe:QUALITY", credits: 3, note: "best quality" },
578
+ ],
579
+ },
580
+
581
+ // ── Google Imagen ──
582
+ "imagen4": {
583
+ id: "imagen4",
584
+ kind: "image",
585
+ modes: ["t2i"] as const,
586
+ family: "Google",
587
+ label: "Imagen 4",
588
+ series: "Imagen",
589
+ description: "Google's Imagen 4 — strong photographic quality and prompt fidelity.",
590
+ useCases: ["photographic", "realistic"],
591
+ aspectRatios: IMAGEN4_RATIOS,
592
+ pricing: [{ identifier: "imagen4", credits: 2 }],
593
+ },
594
+ "imagen4-fast": {
595
+ id: "imagen4-fast",
596
+ kind: "image",
597
+ modes: ["t2i"] as const,
598
+ family: "Google",
599
+ label: "Imagen 4 Fast",
600
+ series: "Imagen",
601
+ description: "Cheaper / quicker Imagen 4 tier.",
602
+ useCases: ["fast", "realistic", "general"],
603
+ aspectRatios: IMAGEN4_RATIOS,
604
+ pricing: [{ identifier: "imagen4-fast", credits: 1 }],
605
+ },
606
+ "imagen4-ultra": {
607
+ id: "imagen4-ultra",
608
+ kind: "image",
609
+ modes: ["t2i"] as const,
610
+ family: "Google",
611
+ label: "Imagen 4 Ultra",
612
+ series: "Imagen",
613
+ description: "Premium Imagen 4 — highest fidelity, slower / more credits.",
614
+ useCases: ["premium", "photographic"],
615
+ aspectRatios: IMAGEN4_RATIOS,
616
+ pricing: [{ identifier: "imagen4-ultra", credits: 3 }],
617
+ },
618
+
619
+ // ── Bytedance Seedream ──
620
+ "seedream": {
621
+ id: "seedream",
622
+ kind: "image",
623
+ modes: ["t2i"] as const,
624
+ family: "Bytedance",
625
+ label: "Seedream 4.5",
626
+ series: "Seedream",
627
+ description: "Bytedance's Seedream 4.5 — precise control, high resolution at 4K via :high.",
628
+ useCases: ["realistic", "high-res", "control"],
629
+ features: ["reference-image"],
630
+ aspectRatios: SEEDREAM_RATIOS,
631
+ qualities: ["basic", "high"],
632
+ pricing: [
633
+ { identifier: "seedream", credits: 2, note: "basic / 2K default" },
634
+ { identifier: "seedream:high", credits: 4, note: "high / 4K" },
635
+ ],
636
+ mcpHidden: true,
637
+ },
638
+ "seedream-edit": {
639
+ id: "seedream-edit",
640
+ kind: "image",
641
+ modes: ["edit"] as const,
642
+ family: "Bytedance",
643
+ label: "Seedream 4.5 Edit",
644
+ series: "Seedream",
645
+ description: "Image editing / transforms via Seedream 4.5.",
646
+ useCases: ["edit", "transform"],
647
+ features: ["reference-image"],
648
+ aspectRatios: SEEDREAM_RATIOS,
649
+ qualities: ["basic", "high"],
650
+ pricing: [
651
+ { identifier: "seedream-edit", credits: 2, note: "basic default" },
652
+ { identifier: "seedream-edit:high", credits: 4, note: "high quality" },
653
+ ],
654
+ mcpHidden: true,
655
+ },
656
+ "seedream-5-lite": {
657
+ id: "seedream-5-lite",
658
+ kind: "image",
659
+ modes: ["t2i"] as const,
660
+ family: "Bytedance",
661
+ label: "Seedream 5 Lite",
662
+ series: "Seedream",
663
+ description: "Newer Seedream 5 Lite — instruction-based generation, visual reasoning.",
664
+ useCases: ["realistic", "instruction"],
665
+ features: ["reference-image"],
666
+ aspectRatios: SEEDREAM_RATIOS,
667
+ qualities: ["basic", "high"],
668
+ pricing: [
669
+ { identifier: "seedream-5-lite", credits: 2, note: "basic default" },
670
+ { identifier: "seedream-5-lite:high", credits: 5, note: "high quality" },
671
+ ],
672
+ },
673
+ "seedream-5-lite-i2i": {
674
+ id: "seedream-5-lite-i2i",
675
+ kind: "image",
676
+ modes: ["i2i"] as const,
677
+ family: "Bytedance",
678
+ label: "Seedream 5 Lite (I2I)",
679
+ series: "Seedream",
680
+ description: "Image-to-image with Seedream 5 Lite.",
681
+ useCases: ["edit", "instruction"],
682
+ features: ["reference-image"],
683
+ aspectRatios: SEEDREAM_RATIOS,
684
+ qualities: ["basic", "high"],
685
+ pricing: [
686
+ { identifier: "seedream-5-lite-i2i", credits: 2, note: "basic default" },
687
+ { identifier: "seedream-5-lite-i2i:high", credits: 5, note: "high quality" },
688
+ ],
689
+ },
690
+
691
+ // ── Alibaba Qwen ──
692
+ "qwen": {
693
+ id: "qwen",
694
+ kind: "image",
695
+ modes: ["t2i"] as const,
696
+ family: "Alibaba",
697
+ label: "Qwen",
698
+ series: "Qwen",
699
+ description: "Cheap, fast, decent quality. Native negative-prompt support.",
700
+ useCases: ["fast", "cheap", "general"],
701
+ features: ["reference-image"],
702
+ aspectRatios: IDEOGRAM_RATIOS,
703
+ pricing: [{ identifier: "qwen", credits: 1 }],
704
+ },
705
+ "qwen-i2i": {
706
+ id: "qwen-i2i",
707
+ kind: "image",
708
+ modes: ["i2i"] as const,
709
+ family: "Alibaba",
710
+ label: "Qwen (I2I)",
711
+ series: "Qwen",
712
+ description: "Image-to-image with Qwen.",
713
+ useCases: ["edit", "fast", "cheap"],
714
+ features: ["reference-image"],
715
+ aspectRatios: IDEOGRAM_RATIOS,
716
+ pricing: [{ identifier: "qwen-i2i", credits: 1 }],
717
+ },
718
+ "qwen-edit": {
719
+ id: "qwen-edit",
720
+ kind: "image",
721
+ modes: ["edit"] as const,
722
+ family: "Alibaba",
723
+ label: "Qwen Edit",
724
+ series: "Qwen",
725
+ description: "Qwen image edit endpoint with native negative prompt.",
726
+ useCases: ["edit", "cheap"],
727
+ features: ["reference-image"],
728
+ aspectRatios: IDEOGRAM_RATIOS,
729
+ pricing: [{ identifier: "qwen-edit", credits: 2 }],
730
+ },
731
+
732
+ // ── Tongyi-MAI Z-Image ──
733
+ "z-image": {
734
+ id: "z-image",
735
+ kind: "image",
736
+ modes: ["t2i"] as const,
737
+ family: "Tongyi-MAI",
738
+ label: "Z-Image",
739
+ series: "Z-Image",
740
+ description: "Cheapest model in catalog. Fast, stylized output. Limited aspect ratios.",
741
+ useCases: ["cheap", "fast", "stylized"],
742
+ aspectRatios: Z_IMAGE_RATIOS,
743
+ pricing: [{ identifier: "z-image", credits: 1 }],
744
+ featured: true,
745
+ },
746
+
747
+ // ── xAI Grok ──
748
+ "grok": {
749
+ id: "grok",
750
+ // Grok serves both t2i (image) and t2v (video) under the same id —
751
+ // backend route resolves which by entry point. We classify the catalog
752
+ // entry by primary kind (image) but list both modes so MCP filters and
753
+ // frontend pickers find it under either context.
754
+ kind: "image",
755
+ modes: ["t2i", "t2v"] as const,
756
+ family: "xAI",
757
+ label: "Grok Imagine",
758
+ series: "Grok",
759
+ description: "Expressive, high-contrast output. Supports both image and video.",
760
+ useCases: ["stylized", "expressive", "general"],
761
+ features: ["reference-image"],
762
+ aspectRatios: GROK_RATIOS,
763
+ pricing: [{ identifier: "grok", credits: 1 }],
764
+ },
765
+ "grok-i2i": {
766
+ id: "grok-i2i",
767
+ kind: "image",
768
+ modes: ["i2i"] as const,
769
+ family: "xAI",
770
+ label: "Grok Imagine (I2I)",
771
+ series: "Grok",
772
+ description: "Image-to-image with Grok.",
773
+ useCases: ["edit", "stylized"],
774
+ features: ["reference-image"],
775
+ pricing: [{ identifier: "grok-i2i", credits: 1 }],
776
+ },
777
+ "grok-upscale": {
778
+ id: "grok-upscale",
779
+ kind: "image",
780
+ modes: ["upscale"] as const,
781
+ family: "xAI",
782
+ label: "Grok Upscale",
783
+ series: "Grok",
784
+ description: "Upscale a previously-generated Grok image. Requires the prior task id.",
785
+ useCases: ["upscale"],
786
+ pricing: [{ identifier: "grok-upscale", credits: 3 }],
787
+ },
788
+
789
+ // ── Utilities ──
790
+ "recraft-remove-bg": {
791
+ id: "recraft-remove-bg",
792
+ kind: "image",
793
+ modes: ["remove-bg"] as const,
794
+ family: "Recraft",
795
+ label: "Recraft Remove BG",
796
+ series: "Recraft",
797
+ description: "Remove image background. Cheap utility.",
798
+ useCases: ["background-removal", "utility"],
799
+ features: ["reference-image"],
800
+ pricing: [{ identifier: "recraft-remove-bg", credits: 1 }],
801
+ },
802
+ "recraft-upscale": {
803
+ id: "recraft-upscale",
804
+ kind: "image",
805
+ modes: ["upscale"] as const,
806
+ family: "Recraft",
807
+ label: "Recraft Crisp Upscale",
808
+ series: "Recraft",
809
+ description: "Light-weight image upscale (Recraft Crisp).",
810
+ useCases: ["upscale", "utility"],
811
+ features: ["reference-image"],
812
+ pricing: [{ identifier: "recraft-upscale", credits: 1 }],
813
+ },
814
+ "topaz-image-upscale": {
815
+ id: "topaz-image-upscale",
816
+ kind: "image",
817
+ modes: ["upscale"] as const,
818
+ family: "Topaz",
819
+ label: "Topaz Image Upscale",
820
+ series: "Topaz",
821
+ description: "High-quality image upscale up to 8K. Best for production-ready output.",
822
+ useCases: ["upscale", "high-res", "premium"],
823
+ features: ["reference-image"],
824
+ resolutions: ["2K", "4K", "8K"],
825
+ pricing: [
826
+ { identifier: "topaz-image-upscale", credits: 3, note: "2K default" },
827
+ { identifier: "topaz-image-upscale:4K", credits: 5, note: "4K" },
828
+ { identifier: "topaz-image-upscale:8K", credits: 10, note: "8K" },
829
+ ],
830
+ },
831
+ }
832
+
833
+ // =============================================================================
834
+ // VIDEO MODELS
835
+ // =============================================================================
836
+ const VIDEO_MODELS: Record<string, ModelCatalogEntry> = {
837
+ // ── Hailuo / MiniMax ──
838
+ "minimax": {
839
+ id: "minimax",
840
+ kind: "video",
841
+ modes: ["i2v", "t2v"] as const,
842
+ family: "MiniMax",
843
+ label: "Hailuo 02 I2V Pro",
844
+ series: "Hailuo",
845
+ description: "Hailuo 02 Pro — strong photoreal motion, fixed 5-second clips. Supports end frame.",
846
+ useCases: ["realistic", "motion", "narrative"],
847
+ features: ["end-frame"],
848
+ durations: [5],
849
+ pricing: [{ identifier: "minimax", credits: 15, note: "5s, 1080p" }],
850
+ },
851
+ "hailuo-2.3-pro": {
852
+ id: "hailuo-2.3-pro",
853
+ kind: "video",
854
+ modes: ["i2v"] as const,
855
+ family: "MiniMax",
856
+ valueLabels: { "1080P": "1080P (6s max)" },
857
+ label: "Hailuo 2.3 Pro",
858
+ series: "Hailuo",
859
+ description: "Hailuo 2.3 Pro — newer Hailuo with 768P / 1080P resolutions.",
860
+ useCases: ["realistic", "motion"],
861
+ durations: [6, 10],
862
+ resolutions: ["768P", "1080P"],
863
+ pricing: [
864
+ { identifier: "hailuo-2.3-pro", credits: 20, note: "10s default" },
865
+ { identifier: "hailuo-2.3-pro:6s", credits: 13, note: "6s" },
866
+ { identifier: "hailuo-2.3-pro:10s", credits: 20, note: "10s" },
867
+ ],
868
+ },
869
+ "hailuo-2.3": {
870
+ id: "hailuo-2.3",
871
+ kind: "video",
872
+ modes: ["i2v"] as const,
873
+ family: "MiniMax",
874
+ resolutions: ["768P", "1080P"],
875
+ valueLabels: { "1080P": "1080P (6s max)" },
876
+ label: "Hailuo 2.3 Standard",
877
+ series: "Hailuo",
878
+ description: "Cheaper Hailuo 2.3 tier — good baseline quality.",
879
+ useCases: ["realistic", "cheap"],
880
+ durations: [6, 10],
881
+ pricing: [
882
+ { identifier: "hailuo-2.3", credits: 8, note: "6s default" },
883
+ { identifier: "hailuo-2.3:6s", credits: 8, note: "6s" },
884
+ { identifier: "hailuo-2.3:10s", credits: 13, note: "10s" },
885
+ ],
886
+ },
887
+ "hailuo-standard": {
888
+ id: "hailuo-standard",
889
+ kind: "video",
890
+ modes: ["i2v", "t2v"] as const,
891
+ family: "MiniMax",
892
+ label: "Hailuo 02 Standard",
893
+ series: "Hailuo",
894
+ description: "Hailuo 02 Standard — economical option with end-frame support.",
895
+ useCases: ["cheap", "motion"],
896
+ features: ["end-frame"],
897
+ durations: [6, 10],
898
+ resolutions: ["512P", "768P"],
899
+ pricing: [
900
+ { identifier: "hailuo-standard", credits: 8, note: "6s default" },
901
+ { identifier: "hailuo-standard:6s", credits: 8, note: "6s" },
902
+ { identifier: "hailuo-standard:10s", credits: 13, note: "10s" },
903
+ ],
904
+ },
905
+
906
+ // ── Google VEO ──
907
+ "veo3": {
908
+ id: "veo3",
909
+ kind: "video",
910
+ modes: ["i2v", "t2v"] as const,
911
+ family: "Google",
912
+ label: "VEO 3.1 Quality",
913
+ series: "VEO",
914
+ description: "Google VEO 3.1 Quality — premium cinematic video. 4/6/8s clips, optional end frame, native audio. Flat per-generation pricing across durations.",
915
+ useCases: ["cinematic", "premium", "narrative"],
916
+ features: ["end-frame", "audio", "reference-image"],
917
+ durations: [4, 6, 8],
918
+ aspectRatios: VIDEO_RATIOS_HV,
919
+ // 720p (default) + 1080p inline. 4K generates the base at 1080p then chains
920
+ // KIE's /api/v1/veo/get-4k-video endpoint (worker handleImageToVideo / handleTextToVideo).
921
+ resolutions: ["720p", "1080p", "4k"],
922
+ pricing: [
923
+ { identifier: "veo3", credits: 63, note: "4/6/8s with audio (flat per-generation)" },
924
+ { identifier: "veo3:4k", credits: 93, note: "4K (base 1080p → get-4k-video)" },
925
+ ],
926
+ featured: true,
927
+ },
928
+ "veo3.1": {
929
+ id: "veo3.1",
930
+ kind: "video",
931
+ modes: ["i2v", "t2v"] as const,
932
+ family: "Google",
933
+ label: "VEO 3.1 Fast",
934
+ series: "VEO",
935
+ description: "VEO 3.1 Fast — cheaper VEO 3.1 tier, 4/6/8s with audio. Good balance for most uses. Flat per-generation pricing across durations.",
936
+ useCases: ["cinematic", "fast", "general"],
937
+ features: ["end-frame", "audio", "reference-image"],
938
+ durations: [4, 6, 8],
939
+ aspectRatios: VIDEO_RATIOS_HV,
940
+ resolutions: ["720p", "1080p", "4k"],
941
+ pricing: [
942
+ { identifier: "veo3.1", credits: 15, note: "4/6/8s with audio @ 720p (flat)" },
943
+ { identifier: "veo3.1:1080p", credits: 17, note: "4/6/8s with audio @ 1080p (flat)" },
944
+ { identifier: "veo3.1:4k", credits: 45, note: "4K (base 1080p → get-4k-video)" },
945
+ ],
946
+ featured: true,
947
+ },
948
+ "veo3_lite": {
949
+ id: "veo3_lite",
950
+ kind: "video",
951
+ modes: ["i2v", "t2v"] as const,
952
+ family: "Google",
953
+ label: "VEO 3.1 Lite",
954
+ series: "VEO",
955
+ description: "VEO 3.1 Lite — most cost-effective VEO tier for high-volume generation. 4/6/8s with audio, supports first+last frame.",
956
+ useCases: ["cinematic", "cheap", "high-volume"],
957
+ features: ["end-frame", "audio", "reference-image"],
958
+ durations: [4, 6, 8],
959
+ aspectRatios: VIDEO_RATIOS_HV,
960
+ resolutions: ["720p", "1080p", "4k"],
961
+ pricing: [
962
+ { identifier: "veo3_lite", credits: 8, note: "4/6/8s with audio @ 720p (flat)" },
963
+ { identifier: "veo3_lite:1080p", credits: 9, note: "4/6/8s with audio @ 1080p (flat)" },
964
+ { identifier: "veo3_lite:4k", credits: 38, note: "4K (base 1080p → get-4k-video)" },
965
+ ],
966
+ featured: false,
967
+ },
968
+ "gemini-omni-video": {
969
+ id: "gemini-omni-video",
970
+ kind: "video",
971
+ modes: ["i2v", "t2v"] as const, // V2V is served via the i2v handle, not a catalog mode
972
+ family: "Google",
973
+ label: "Gemini Omni",
974
+ series: "Gemini",
975
+ description: "Google multimodal video with native audio; text/image-to-video + video-edit.",
976
+ useCases: ["cinematic", "narrative"],
977
+ features: ["audio", "reference-image"],
978
+ aspectRatios: VIDEO_RATIOS_HV,
979
+ resolutions: ["720p", "1080p", "4k"],
980
+ durations: [4, 6, 8, 10],
981
+ pricing: [
982
+ { identifier: "gemini-omni-video", credits: 23, note: "default — 720p/1080p 4s" },
983
+ { identifier: "gemini-omni-video:4", credits: 23, note: "720p/1080p 4s" },
984
+ { identifier: "gemini-omni-video:6", credits: 30, note: "720p/1080p 6s" },
985
+ { identifier: "gemini-omni-video:8", credits: 38, note: "720p/1080p 8s" },
986
+ { identifier: "gemini-omni-video:10", credits: 45, note: "720p/1080p 10s" },
987
+ { identifier: "gemini-omni-video:4k:4", credits: 53, note: "4K 4s" },
988
+ { identifier: "gemini-omni-video:4k:6", credits: 60, note: "4K 6s" },
989
+ { identifier: "gemini-omni-video:4k:8", credits: 68, note: "4K 8s" },
990
+ { identifier: "gemini-omni-video:4k:10", credits: 75, note: "4K 10s" },
991
+ { identifier: "gemini-omni-video:vref", credits: 60, note: "video-edit 720p/1080p" },
992
+ { identifier: "gemini-omni-video:4k:vref", credits: 90, note: "video-edit 4K" },
993
+ ],
994
+ },
995
+
996
+ // ── Kling ──
997
+ "kling": {
998
+ id: "kling",
999
+ kind: "video",
1000
+ modes: ["i2v", "t2v"] as const,
1001
+ family: "Kuaishou",
1002
+ label: "Kling 2.6",
1003
+ series: "Kling",
1004
+ description: "Kling 2.6 I2V — strong motion realism. 5s/10s, optional native audio.",
1005
+ useCases: ["realistic", "motion", "dance"],
1006
+ features: ["audio"],
1007
+ aspectRatios: VIDEO_RATIOS_HVS,
1008
+ durations: [5, 10],
1009
+ pricing: [
1010
+ { identifier: "kling", credits: 28, note: "10s no audio default" },
1011
+ { identifier: "kling:5s", credits: 14, note: "5s no audio" },
1012
+ { identifier: "kling:10s", credits: 28, note: "10s no audio" },
1013
+ { identifier: "kling:5s:audio", credits: 28, note: "5s with audio" },
1014
+ { identifier: "kling:10s:audio", credits: 55, note: "10s with audio" },
1015
+ ],
1016
+ },
1017
+ "kling-turbo": {
1018
+ id: "kling-turbo",
1019
+ kind: "video",
1020
+ modes: ["i2v", "t2v"] as const,
1021
+ family: "Kuaishou",
1022
+ label: "Kling 2.5 Turbo Pro",
1023
+ series: "Kling",
1024
+ description: "Faster Kling — good quality at lower cost. Supports end frame.",
1025
+ useCases: ["fast", "motion", "cheap"],
1026
+ features: ["end-frame"],
1027
+ durations: [5, 10],
1028
+ pricing: [
1029
+ { identifier: "kling-turbo", credits: 11, note: "5s default" },
1030
+ { identifier: "kling-turbo:5s", credits: 11 },
1031
+ { identifier: "kling-turbo:10s", credits: 21 },
1032
+ ],
1033
+ },
1034
+ "kling-3.0": {
1035
+ id: "kling-3.0",
1036
+ kind: "video",
1037
+ modes: ["i2v", "t2v"] as const,
1038
+ family: "Kuaishou",
1039
+ label: "Kling 3.0",
1040
+ series: "Kling",
1041
+ description: "Premium Kling 3.0 — variable 3-15s duration, native audio, 720P/1080P.",
1042
+ useCases: ["premium", "narrative", "cinematic"],
1043
+ features: ["end-frame", "audio"],
1044
+ durations: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
1045
+ resolutions: ["720P", "1080P"],
1046
+ pricing: [
1047
+ { identifier: "kling-3.0", credits: 50, note: "5s 1080p with audio" },
1048
+ { identifier: "kling-3.0:5s", credits: 34, note: "5s 1080p no audio" },
1049
+ { identifier: "kling-3.0:10s", credits: 68, note: "10s 1080p no audio" },
1050
+ { identifier: "kling-3.0:15s", credits: 102, note: "15s 1080p no audio" },
1051
+ { identifier: "kling-3.0:5s:audio", credits: 50, note: "5s 1080p with audio" },
1052
+ { identifier: "kling-3.0:10s:audio", credits: 100, note: "10s 1080p with audio" },
1053
+ { identifier: "kling-3.0:15s:audio", credits: 150, note: "15s 1080p with audio" },
1054
+ ],
1055
+ featured: true,
1056
+ },
1057
+ "kling-master": {
1058
+ id: "kling-master",
1059
+ kind: "video",
1060
+ modes: ["i2v"] as const,
1061
+ family: "Kuaishou",
1062
+ label: "Kling 2.1 Master",
1063
+ series: "Kling",
1064
+ description: "Master tier I2V — strong cinematic quality.",
1065
+ useCases: ["cinematic", "premium"],
1066
+ durations: [5, 10],
1067
+ pricing: [
1068
+ { identifier: "kling-master", credits: 40, note: "5s default" },
1069
+ { identifier: "kling-master:5s", credits: 40 },
1070
+ { identifier: "kling-master:10s", credits: 80 },
1071
+ ],
1072
+ },
1073
+ "kling-3-omni": {
1074
+ id: "kling-3-omni",
1075
+ kind: "video",
1076
+ modes: ["i2v"] as const,
1077
+ family: "Kuaishou",
1078
+ label: "Kling 3 Omni",
1079
+ series: "Kling",
1080
+ description: "Kling 3 Omni via Replicate — 3-15s, 720p/1080p, end frame + reference images, native audio.",
1081
+ useCases: ["motion", "cinematic", "reference"],
1082
+ features: ["end-frame", "reference-image"],
1083
+ durations: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
1084
+ resolutions: ["720p", "1080p"],
1085
+ aspectRatios: VIDEO_RATIOS_HVS,
1086
+ pricing: [
1087
+ { identifier: "kling-3-omni", credits: 25, note: "5s 720p default" },
1088
+ { identifier: "kling-3-omni:5s", credits: 25 },
1089
+ { identifier: "kling-3-omni:10s", credits: 50 },
1090
+ { identifier: "kling-3-omni:15s", credits: 75 },
1091
+ ],
1092
+ },
1093
+
1094
+ // ── xAI Grok video ──
1095
+ "grok-i2v": {
1096
+ id: "grok-i2v",
1097
+ kind: "video",
1098
+ modes: ["i2v"] as const,
1099
+ features: ["reference-image"],
1100
+ family: "xAI",
1101
+ label: "Grok Imagine (I2V)",
1102
+ series: "Grok",
1103
+ description: "Grok image-to-video — stylized motion. Up to 15s.",
1104
+ useCases: ["stylized", "motion"],
1105
+ durations: [6, 10],
1106
+ resolutions: ["480p", "720p"],
1107
+ pricing: [
1108
+ { identifier: "grok-i2v", credits: 5, note: "6s default" },
1109
+ { identifier: "grok-i2v:6s", credits: 5 },
1110
+ { identifier: "grok-i2v:10s", credits: 8 },
1111
+ { identifier: "grok-i2v:15s", credits: 10 },
1112
+ ],
1113
+ },
1114
+ // xAI Grok Imagine Video 1.5 — KIE image-to-video (image required). True
1115
+ // per-second billing split 480p/720p. modes:["i2v"] is the honest capability;
1116
+ // it's also surfaced in the text-to-video dropdown for the unified node, where
1117
+ // the t2v route returns a clean "requires an input image" 400.
1118
+ "grok-imagine-video-1.5": {
1119
+ id: "grok-imagine-video-1.5",
1120
+ kind: "video",
1121
+ modes: ["i2v"] as const,
1122
+ // No `reference-image` (locked by video-reference-features.test.ts, 2026-06-28
1123
+ // audit): KIE's grok-imagine endpoint accepts a single image_url (the i2v
1124
+ // start frame) only — there is no reference transport, so advertising the
1125
+ // feature let the editor attach refs that were silently dropped at the
1126
+ // provider. Also kept OUT of VIDEO_REF_LIMITS_BY_PROVIDER for the same reason.
1127
+ family: "xAI",
1128
+ label: "Grok Imagine Video 1.5",
1129
+ series: "Grok",
1130
+ description: "Grok Imagine 1.5 image-to-video — 1–15s, 480p/720p, per-second pricing. Requires an input image.",
1131
+ useCases: ["stylized", "motion"],
1132
+ aspectRatios: ["16:9", "9:16", "1:1", "4:3", "3:4"],
1133
+ durations: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
1134
+ resolutions: ["480p", "720p"],
1135
+ pricing: [
1136
+ { identifier: "grok-imagine-video-1.5", credits: 30, note: "default 8s 480p — see :Ns:res variants" },
1137
+ { identifier: "grok-imagine-video-1.5:8s:480p", credits: 30, note: "8s 480p" },
1138
+ { identifier: "grok-imagine-video-1.5:8s:720p", credits: 51, note: "8s 720p" },
1139
+ { identifier: "grok-imagine-video-1.5:15s:480p", credits: 55, note: "15s 480p (max)" },
1140
+ { identifier: "grok-imagine-video-1.5:15s:720p", credits: 95, note: "15s 720p (max)" },
1141
+ ],
1142
+ },
1143
+
1144
+ // ── Bytedance Seedance ──
1145
+ "seedance": {
1146
+ id: "seedance",
1147
+ kind: "video",
1148
+ modes: ["i2v", "t2v"] as const,
1149
+ family: "Bytedance",
1150
+ label: "Seedance 1.5 Pro",
1151
+ series: "Seedance",
1152
+ description: "Bytedance Seedance 1.5 Pro — flexible duration (4/8/12s) with end-frame support.",
1153
+ useCases: ["motion", "narrative"],
1154
+ features: ["end-frame"],
1155
+ durations: [4, 8, 12],
1156
+ resolutions: ["480p", "720p", "1080p"],
1157
+ pricing: [
1158
+ { identifier: "seedance", credits: 7, note: "8s default" },
1159
+ { identifier: "seedance:4s", credits: 4 },
1160
+ { identifier: "seedance:8s", credits: 7 },
1161
+ { identifier: "seedance:12s", credits: 15 },
1162
+ ],
1163
+ mcpHidden: true,
1164
+ },
1165
+ "seedance-2": {
1166
+ id: "seedance-2",
1167
+ kind: "video",
1168
+ modes: ["i2v", "t2v"] as const,
1169
+ family: "Bytedance",
1170
+ label: "Seedance 2",
1171
+ series: "Seedance",
1172
+ description: "Seedance 2 — premium tier with native audio. Per-second pricing by resolution.",
1173
+ useCases: ["premium", "narrative"],
1174
+ features: ["end-frame", "audio", "reference-image"],
1175
+ aspectRatios: VIDEO_RATIOS_SEEDANCE_2,
1176
+ durations: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
1177
+ resolutions: ["480p", "720p", "1080p", "4k"],
1178
+ pricing: [
1179
+ { identifier: "seedance-2", credits: 38, note: "default — see :NsR variants for exact" },
1180
+ { identifier: "seedance-2:8s:480p", credits: 38, note: "8s 480p" },
1181
+ { identifier: "seedance-2:8s:720p", credits: 82, note: "8s 720p" },
1182
+ { identifier: "seedance-2:8s:1080p", credits: 204, note: "8s 1080p" },
1183
+ { identifier: "seedance-2:8s:480p-ref", credits: 23, note: "8s 480p with reference" },
1184
+ { identifier: "seedance-2:8s:720p-ref", credits: 50, note: "8s 720p with reference" },
1185
+ { identifier: "seedance-2:8s:1080p-ref", credits: 124, note: "8s 1080p with reference" },
1186
+ { identifier: "seedance-2:8s:4k", credits: 416, note: "8s 4k" },
1187
+ { identifier: "seedance-2:8s:4k-ref", credits: 256, note: "8s 4k with reference" },
1188
+ ],
1189
+ },
1190
+ "seedance-2-fast": {
1191
+ id: "seedance-2-fast",
1192
+ kind: "video",
1193
+ modes: ["i2v", "t2v"] as const,
1194
+ family: "Bytedance",
1195
+ label: "Seedance 2 Fast",
1196
+ series: "Seedance",
1197
+ description: "Cheaper / quicker Seedance 2 tier.",
1198
+ useCases: ["fast", "motion"],
1199
+ features: ["end-frame", "audio", "reference-image"],
1200
+ aspectRatios: VIDEO_RATIOS_SEEDANCE_2,
1201
+ durations: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
1202
+ resolutions: ["480p", "720p"],
1203
+ pricing: [
1204
+ { identifier: "seedance-2-fast", credits: 31, note: "default — see :NsR variants" },
1205
+ { identifier: "seedance-2-fast:8s:480p", credits: 31 },
1206
+ { identifier: "seedance-2-fast:8s:720p", credits: 66 },
1207
+ { identifier: "seedance-2-fast:8s:480p-ref", credits: 18 },
1208
+ { identifier: "seedance-2-fast:8s:720p-ref", credits: 40 },
1209
+ ],
1210
+ },
1211
+ "seedance-2-mini": {
1212
+ id: "seedance-2-mini",
1213
+ kind: "video",
1214
+ modes: ["i2v", "t2v"] as const,
1215
+ family: "Bytedance",
1216
+ label: "Seedance 2 Mini",
1217
+ series: "Seedance",
1218
+ description: "Budget Seedance 2 tier — 480p/720p only, per-second pricing by resolution.",
1219
+ useCases: ["fast", "motion"],
1220
+ features: ["end-frame", "audio", "reference-image"],
1221
+ aspectRatios: VIDEO_RATIOS_SEEDANCE_2,
1222
+ durations: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
1223
+ resolutions: ["480p", "720p"],
1224
+ pricing: [
1225
+ { identifier: "seedance-2-mini", credits: 19, note: "default — see :NsR variants" },
1226
+ { identifier: "seedance-2-mini:8s:480p", credits: 19 },
1227
+ { identifier: "seedance-2-mini:8s:720p", credits: 41 },
1228
+ { identifier: "seedance-2-mini:8s:480p-ref", credits: 12 },
1229
+ { identifier: "seedance-2-mini:8s:720p-ref", credits: 25 },
1230
+ ],
1231
+ },
1232
+
1233
+ // ── Wan ──
1234
+ "wan-i2v": {
1235
+ id: "wan-i2v",
1236
+ kind: "video",
1237
+ modes: ["i2v"] as const,
1238
+ family: "Alibaba",
1239
+ label: "Wan 2.6 I2V",
1240
+ series: "Wan",
1241
+ description: "Wan 2.6 image-to-video — 5/10/15s at 720p/1080p.",
1242
+ useCases: ["motion", "narrative"],
1243
+ durations: [5, 10, 15],
1244
+ resolutions: ["720p", "1080p"],
1245
+ pricing: [
1246
+ { identifier: "wan-i2v", credits: 18, note: "5s 720p default" },
1247
+ { identifier: "wan-i2v:5s", credits: 18 },
1248
+ { identifier: "wan-i2v:10s", credits: 35 },
1249
+ { identifier: "wan-i2v:15s", credits: 53 },
1250
+ ],
1251
+ },
1252
+ "wan-turbo": {
1253
+ id: "wan-turbo",
1254
+ kind: "video",
1255
+ modes: ["i2v", "t2v"] as const,
1256
+ family: "Alibaba",
1257
+ label: "Wan 2.2 Turbo",
1258
+ series: "Wan",
1259
+ description: "Cheap, fast Wan turbo — 5s. Serves both i2v and t2v under one id.",
1260
+ useCases: ["cheap", "fast"],
1261
+ aspectRatios: VIDEO_RATIOS_HVS,
1262
+ durations: [5],
1263
+ resolutions: ["480p", "720p"],
1264
+ // Different KIE endpoints (i2v vs t2v) → different costs under
1265
+ // composite ids. Route picks endpoint based on whether image was supplied.
1266
+ pricing: [
1267
+ { identifier: "wan-turbo", credits: 10, note: "i2v 5s 480p" },
1268
+ { identifier: "wan-turbo-t2v", credits: 20, note: "t2v 5s 720p" },
1269
+ ],
1270
+ },
1271
+ "wan": {
1272
+ id: "wan",
1273
+ kind: "video",
1274
+ // "wan" id covers v2v AND t2v under different KIE endpoints; the
1275
+ // pricing variants tag which mode each cost belongs to.
1276
+ modes: ["v2v", "t2v"] as const,
1277
+ family: "Alibaba",
1278
+ label: "Wan 2.6",
1279
+ series: "Wan",
1280
+ description: "Wan 2.6 — text-to-video and video-to-video under a single id.",
1281
+ useCases: ["v2v", "t2v", "restyle"],
1282
+ durations: [5],
1283
+ resolutions: ["720p", "1080p"],
1284
+ pricing: [
1285
+ { identifier: "wan", credits: 18, note: "v2v 5s 720p" },
1286
+ { identifier: "wan-t2v", credits: 27, note: "t2v 5s 1080p" },
1287
+ ],
1288
+ },
1289
+ "wan-flash": {
1290
+ id: "wan-flash",
1291
+ kind: "video",
1292
+ modes: ["v2v"] as const,
1293
+ family: "Alibaba",
1294
+ label: "Wan Flash V2V",
1295
+ series: "Wan",
1296
+ description: "Faster Wan V2V variant.",
1297
+ useCases: ["v2v", "fast"],
1298
+ pricing: [{ identifier: "wan-flash", credits: 13 }],
1299
+ },
1300
+ "wan-videoedit": {
1301
+ id: "wan-videoedit",
1302
+ kind: "video",
1303
+ modes: ["v2v"] as const,
1304
+ family: "Alibaba",
1305
+ label: "Wan 2.7 VideoEdit",
1306
+ series: "Wan",
1307
+ description: "Guided video editing with optional reference image, audio control, and prompt expansion.",
1308
+ useCases: ["v2v", "restyle", "reference"],
1309
+ resolutions: ["720p", "1080p"],
1310
+ pricing: [{ identifier: "wan-videoedit", credits: 32 }],
1311
+ },
1312
+ // ── Wan 2.7 ──
1313
+ "wan-2.7": {
1314
+ id: "wan-2.7",
1315
+ kind: "image",
1316
+ modes: ["t2i"] as const,
1317
+ family: "Alibaba",
1318
+ label: "Wan 2.7",
1319
+ series: "Wan",
1320
+ description: "Wan 2.7 text-to-image — 1K/2K/4K, up to 9 optional style/character reference images.",
1321
+ useCases: ["photorealistic", "stylized", "reference"],
1322
+ features: ["reference-image"] as const,
1323
+ aspectRatios: WAN_27_IMAGE_RATIOS,
1324
+ resolutions: ["1K", "2K", "4K"],
1325
+ pricing: [
1326
+ { identifier: "wan-2.7", credits: 2, note: "1K base" },
1327
+ { identifier: "wan-2.7:2K", credits: 4, note: "2K" },
1328
+ { identifier: "wan-2.7:4K", credits: 8, note: "4K" },
1329
+ ],
1330
+ },
1331
+ "wan-2.7-pro": {
1332
+ id: "wan-2.7-pro",
1333
+ kind: "image",
1334
+ modes: ["t2i"] as const,
1335
+ family: "Alibaba",
1336
+ label: "Wan 2.7 Pro",
1337
+ series: "Wan",
1338
+ description: "Wan 2.7 Pro text-to-image — higher quality, 1K/2K/4K, no image input.",
1339
+ useCases: ["photorealistic", "premium"],
1340
+ aspectRatios: WAN_27_IMAGE_RATIOS,
1341
+ resolutions: ["1K", "2K", "4K"],
1342
+ pricing: [
1343
+ { identifier: "wan-2.7-pro", credits: 3, note: "1K base" },
1344
+ { identifier: "wan-2.7-pro:2K", credits: 6, note: "2K" },
1345
+ { identifier: "wan-2.7-pro:4K", credits: 12, note: "4K" },
1346
+ ],
1347
+ },
1348
+ "wan-2.7-i2v": {
1349
+ id: "wan-2.7-i2v",
1350
+ kind: "video",
1351
+ modes: ["i2v"] as const,
1352
+ family: "Alibaba",
1353
+ label: "Wan 2.7 I2V",
1354
+ series: "Wan",
1355
+ description: "Wan 2.7 image-to-video — 2–15s at 720p/1080p, supports start+end frame.",
1356
+ useCases: ["motion", "narrative"],
1357
+ durations: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
1358
+ resolutions: ["720p", "1080p"],
1359
+ features: ["end-frame"],
1360
+ pricing: [
1361
+ { identifier: "wan-2.7-i2v", credits: 19, note: "5s 720p default" },
1362
+ ],
1363
+ },
1364
+ "wan-2.7-t2v": {
1365
+ id: "wan-2.7-t2v",
1366
+ kind: "video",
1367
+ modes: ["t2v"] as const,
1368
+ family: "Alibaba",
1369
+ label: "Wan 2.7 T2V",
1370
+ series: "Wan",
1371
+ description: "Wan 2.7 text-to-video — 2–15s at 720p/1080p.",
1372
+ useCases: ["motion", "narrative"],
1373
+ aspectRatios: VIDEO_RATIOS_HVS345,
1374
+ durations: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
1375
+ resolutions: ["720p", "1080p"],
1376
+ pricing: [
1377
+ { identifier: "wan-2.7-t2v", credits: 19, note: "5s 720p default" },
1378
+ ],
1379
+ },
1380
+ // ── HappyHorse ──
1381
+ "happyhorse": {
1382
+ id: "happyhorse",
1383
+ kind: "video",
1384
+ modes: ["t2v"] as const,
1385
+ family: "HappyHorse",
1386
+ label: "HappyHorse",
1387
+ series: "HappyHorse",
1388
+ description: "HappyHorse text-to-video — 3–15s at 720p/1080p.",
1389
+ useCases: ["motion", "creative"],
1390
+ aspectRatios: VIDEO_RATIOS_HVS345,
1391
+ durations: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
1392
+ resolutions: ["720p", "1080p"],
1393
+ pricing: [
1394
+ { identifier: "happyhorse", credits: 13, note: "5s 720p default" },
1395
+ ],
1396
+ },
1397
+ "happyhorse-i2v": {
1398
+ id: "happyhorse-i2v",
1399
+ kind: "video",
1400
+ modes: ["i2v"] as const,
1401
+ family: "HappyHorse",
1402
+ label: "HappyHorse I2V",
1403
+ series: "HappyHorse",
1404
+ description: "HappyHorse image-to-video — 3–15s at 720p/1080p, aspect ratio inferred from input image.",
1405
+ useCases: ["motion", "creative"],
1406
+ durations: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
1407
+ resolutions: ["720p", "1080p"],
1408
+ pricing: [
1409
+ { identifier: "happyhorse-i2v", credits: 13, note: "5s 720p default" },
1410
+ ],
1411
+ },
1412
+ "happyhorse-ref2v": {
1413
+ id: "happyhorse-ref2v",
1414
+ kind: "video",
1415
+ modes: ["i2v"] as const,
1416
+ family: "HappyHorse",
1417
+ label: "HappyHorse Ref2V",
1418
+ series: "HappyHorse",
1419
+ description: "HappyHorse reference-to-video — 1–9 reference images, 3–15s at 720p/1080p.",
1420
+ useCases: ["motion", "reference"],
1421
+ aspectRatios: VIDEO_RATIOS_HVS345,
1422
+ durations: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
1423
+ resolutions: ["720p", "1080p"],
1424
+ features: ["reference-image"],
1425
+ pricing: [
1426
+ { identifier: "happyhorse-ref2v", credits: 15, note: "5s 720p default" },
1427
+ ],
1428
+ },
1429
+ "happyhorse-edit": {
1430
+ id: "happyhorse-edit",
1431
+ kind: "video",
1432
+ modes: ["v2v"] as const,
1433
+ family: "HappyHorse",
1434
+ label: "HappyHorse Edit",
1435
+ series: "HappyHorse",
1436
+ description: "HappyHorse video-edit — video-to-video transformation, up to 60s input, 720p/1080p output.",
1437
+ useCases: ["v2v", "restyle"],
1438
+ resolutions: ["720p", "1080p"],
1439
+ pricing: [
1440
+ { identifier: "happyhorse-edit", credits: 20, note: "720p default" },
1441
+ ],
1442
+ },
1443
+ // ── Bytedance video lite/pro ──
1444
+ "bytedance-lite": {
1445
+ id: "bytedance-lite",
1446
+ kind: "video",
1447
+ modes: ["i2v", "t2v"] as const,
1448
+ family: "Bytedance",
1449
+ label: "Bytedance Lite I2V",
1450
+ series: "Bytedance",
1451
+ description: "Cheapest Bytedance video tier with end-frame support.",
1452
+ useCases: ["cheap", "motion"],
1453
+ features: ["end-frame"],
1454
+ aspectRatios: VIDEO_RATIOS_HVS,
1455
+ durations: [5, 10],
1456
+ resolutions: ["480p", "720p", "1080p"],
1457
+ pricing: [{ identifier: "bytedance-lite", credits: 6 }],
1458
+ },
1459
+ "bytedance-pro": {
1460
+ id: "bytedance-pro",
1461
+ kind: "video",
1462
+ modes: ["i2v", "t2v"] as const,
1463
+ family: "Bytedance",
1464
+ label: "Bytedance Pro I2V",
1465
+ series: "Bytedance",
1466
+ description: "Pro Bytedance video tier — better quality.",
1467
+ useCases: ["motion", "narrative"],
1468
+ aspectRatios: VIDEO_RATIOS_HVS,
1469
+ durations: [5, 10],
1470
+ resolutions: ["480p", "720p", "1080p"],
1471
+ pricing: [{ identifier: "bytedance-pro", credits: 18 }],
1472
+ },
1473
+ "bytedance-pro-fast": {
1474
+ id: "bytedance-pro-fast",
1475
+ kind: "video",
1476
+ modes: ["i2v"] as const,
1477
+ family: "Bytedance",
1478
+ label: "Bytedance Pro Fast I2V",
1479
+ series: "Bytedance",
1480
+ description: "Faster Bytedance Pro variant.",
1481
+ useCases: ["fast", "motion"],
1482
+ durations: [5, 10],
1483
+ resolutions: ["720p", "1080p"],
1484
+ pricing: [{ identifier: "bytedance-pro-fast", credits: 9 }],
1485
+ },
1486
+
1487
+ // ── OpenAI Sora ──
1488
+ // (Sora2 / Sora2-Pro are listed in KIE models but not in STATIC_CREDIT_COSTS;
1489
+ // they're priced at runtime via the per-second tracker, so we omit pricing
1490
+ // arrays with composite entries — `list_models` will fall back to whatever
1491
+ // base entry exists.)
1492
+
1493
+ // ── Runway ──
1494
+ "runway-kie": {
1495
+ id: "runway-kie",
1496
+ kind: "video",
1497
+ modes: ["i2v", "t2v"] as const,
1498
+ family: "Runway",
1499
+ label: "Runway (via KIE)",
1500
+ series: "Runway",
1501
+ description: "Runway Gen-3 routed through KIE. 5/10s at 720p/1080p.",
1502
+ useCases: ["motion", "narrative"],
1503
+ durations: [5, 10],
1504
+ resolutions: ["720p", "1080p"],
1505
+ pricing: [{ identifier: "runway-kie", credits: 3, note: "5s 720p" }],
1506
+ },
1507
+ "runway-aleph": {
1508
+ id: "runway-aleph",
1509
+ kind: "video",
1510
+ modes: ["v2v"] as const,
1511
+ family: "Runway",
1512
+ label: "Runway Aleph V2V",
1513
+ series: "Runway",
1514
+ description: "Runway Aleph — video-to-video conversion.",
1515
+ useCases: ["v2v", "restyle"],
1516
+ pricing: [{ identifier: "runway-aleph", credits: 35, note: "5s clip" }],
1517
+ },
1518
+
1519
+ // ── Extend / Upscale ──
1520
+ "veo-extend": {
1521
+ id: "veo-extend",
1522
+ kind: "video",
1523
+ modes: ["extend"] as const,
1524
+ family: "Google",
1525
+ label: "VEO Extend",
1526
+ series: "VEO",
1527
+ description: "Extend an existing VEO 3.1 clip by another segment.",
1528
+ useCases: ["extend"],
1529
+ pricing: [
1530
+ { identifier: "veo-extend", credits: 19, note: "VEO 3.1 Fast default" },
1531
+ { identifier: "veo-extend:quality", credits: 79, note: "VEO 3.1 Quality" },
1532
+ ],
1533
+ },
1534
+ "runway-extend": {
1535
+ id: "runway-extend",
1536
+ kind: "video",
1537
+ modes: ["extend"] as const,
1538
+ family: "Runway",
1539
+ label: "Runway Extend",
1540
+ series: "Runway",
1541
+ description: "Extend a Runway video by another clip.",
1542
+ useCases: ["extend"],
1543
+ pricing: [{ identifier: "runway-extend", credits: 32 }],
1544
+ },
1545
+ "seedance-2-extend": {
1546
+ id: "seedance-2-extend",
1547
+ kind: "video",
1548
+ modes: ["extend"] as const,
1549
+ family: "Bytedance",
1550
+ label: "Seedance 2 Extend",
1551
+ series: "Seedance",
1552
+ description: "Extend ANY video: generates the continuation (audio included) and trim-stitches it into one seamless clip.",
1553
+ useCases: ["extend"],
1554
+ durations: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
1555
+ resolutions: ["480p", "720p", "1080p"],
1556
+ pricing: [
1557
+ { identifier: "seedance-2-extend", credits: 53, note: "default 8s 720p (ref-mode 50 + 3 stitch) — see :NsR variants" },
1558
+ { identifier: "seedance-2-extend:8s:480p", credits: 26, note: "8s 480p (23+3)" },
1559
+ { identifier: "seedance-2-extend:8s:720p", credits: 53, note: "8s 720p (50+3)" },
1560
+ { identifier: "seedance-2-extend:8s:1080p", credits: 78, note: "8s 1080p (75+3)" },
1561
+ ],
1562
+ },
1563
+ "veo-1080p": {
1564
+ id: "veo-1080p",
1565
+ kind: "video",
1566
+ modes: ["video-upscale"] as const,
1567
+ family: "Google",
1568
+ label: "VEO 1080p Upscale",
1569
+ series: "VEO",
1570
+ description: "Upscale VEO output to 1080p.",
1571
+ useCases: ["upscale"],
1572
+ pricing: [{ identifier: "veo-1080p", credits: 2 }],
1573
+ },
1574
+ "veo-4k": {
1575
+ id: "veo-4k",
1576
+ kind: "video",
1577
+ modes: ["video-upscale"] as const,
1578
+ family: "Google",
1579
+ label: "VEO 4K Upscale",
1580
+ series: "VEO",
1581
+ description: "Upscale VEO output to 4K.",
1582
+ useCases: ["upscale", "high-res"],
1583
+ pricing: [{ identifier: "veo-4k", credits: 38 }],
1584
+ },
1585
+ "topaz-video": {
1586
+ id: "topaz-video",
1587
+ kind: "video",
1588
+ modes: ["video-upscale"] as const,
1589
+ family: "Topaz",
1590
+ label: "Topaz Video Upscale",
1591
+ series: "Topaz",
1592
+ description: "High-quality video upscale and enhancement.",
1593
+ useCases: ["upscale", "premium"],
1594
+ pricing: [{ identifier: "topaz-video", credits: 19, note: "~5s clip" }],
1595
+ },
1596
+
1597
+ // ── Motion transfer ──
1598
+ "motion-transfer": {
1599
+ id: "motion-transfer",
1600
+ kind: "video",
1601
+ modes: ["motion-transfer"] as const,
1602
+ family: "Kuaishou",
1603
+ label: "Kling 2.6 Motion Transfer",
1604
+ series: "Kling",
1605
+ description: "Transfer the motion from a driving video onto a still subject. Kling 2.6 base.",
1606
+ useCases: ["motion-transfer"],
1607
+ durations: [5, 10, 15, 30],
1608
+ resolutions: ["720p", "1080p"],
1609
+ pricing: [
1610
+ { identifier: "motion-transfer", credits: 15, note: "10s 720p default" },
1611
+ { identifier: "motion-transfer:5s", credits: 8 },
1612
+ { identifier: "motion-transfer:10s", credits: 15 },
1613
+ { identifier: "motion-transfer:15s", credits: 23 },
1614
+ { identifier: "motion-transfer:30s", credits: 45 },
1615
+ { identifier: "motion-transfer:1080p:10s", credits: 23 },
1616
+ ],
1617
+ },
1618
+ "kling-3.0-motion": {
1619
+ id: "kling-3.0-motion",
1620
+ kind: "video",
1621
+ modes: ["motion-transfer"] as const,
1622
+ family: "Kuaishou",
1623
+ label: "Kling 3.0 Motion Transfer",
1624
+ series: "Kling",
1625
+ description: "Premium motion transfer via Kling 3.0.",
1626
+ useCases: ["motion-transfer", "premium"],
1627
+ durations: [5, 10, 15, 30],
1628
+ resolutions: ["720p", "1080p"],
1629
+ pricing: [
1630
+ { identifier: "kling-3.0-motion", credits: 30, note: "10s 720p default" },
1631
+ { identifier: "kling-3.0-motion:5s", credits: 15 },
1632
+ { identifier: "kling-3.0-motion:10s", credits: 30 },
1633
+ { identifier: "kling-3.0-motion:1080p:10s", credits: 50 },
1634
+ ],
1635
+ },
1636
+
1637
+ // ── Lip sync / talking-head ──
1638
+ "kling-avatar": {
1639
+ id: "kling-avatar",
1640
+ kind: "video",
1641
+ modes: ["lip-sync"] as const,
1642
+ family: "Kuaishou",
1643
+ label: "Kling Avatar Standard",
1644
+ series: "Kling",
1645
+ description: "Lip-sync a still portrait to driving audio. Standard quality.",
1646
+ useCases: ["lip-sync", "talking-head"],
1647
+ pricing: [{ identifier: "kling-avatar", credits: 28, note: "~14s default" }],
1648
+ },
1649
+ "kling-avatar-pro": {
1650
+ id: "kling-avatar-pro",
1651
+ kind: "video",
1652
+ modes: ["lip-sync"] as const,
1653
+ family: "Kuaishou",
1654
+ label: "Kling Avatar Pro",
1655
+ series: "Kling",
1656
+ description: "Premium lip-sync — better mouth shape and timing.",
1657
+ useCases: ["lip-sync", "talking-head", "premium"],
1658
+ pricing: [{ identifier: "kling-avatar-pro", credits: 56 }],
1659
+ },
1660
+ "infinitalk": {
1661
+ id: "infinitalk",
1662
+ kind: "video",
1663
+ modes: ["lip-sync"] as const,
1664
+ family: "InfiniTalk",
1665
+ label: "InfiniTalk",
1666
+ series: "InfiniTalk",
1667
+ description: "Audio-driven talking-head from a still image. 480p / 720p.",
1668
+ useCases: ["lip-sync", "talking-head"],
1669
+ resolutions: ["480p", "720p"],
1670
+ pricing: [
1671
+ { identifier: "infinitalk", credits: 42, note: "720p default" },
1672
+ { identifier: "infinitalk:480p", credits: 11 },
1673
+ { identifier: "infinitalk:720p", credits: 42 },
1674
+ ],
1675
+ },
1676
+ "omnihuman-1-5": {
1677
+ id: "omnihuman-1-5",
1678
+ kind: "video",
1679
+ modes: ["lip-sync"] as const,
1680
+ family: "ByteDance",
1681
+ label: "OmniHuman 1.5",
1682
+ series: "OmniHuman",
1683
+ description: "Premium prompt-directed talking avatar from a still image + audio. 720p / 1080p, up to 60s. People, pets, anime.",
1684
+ useCases: ["lip-sync", "talking-head", "premium"],
1685
+ resolutions: ["720p", "1080p"],
1686
+ pricing: [
1687
+ { identifier: "omnihuman-1-5", credits: 405, note: "60s ceiling (no duration given)" },
1688
+ { identifier: "omnihuman-1-5:15s", credits: 102 },
1689
+ { identifier: "omnihuman-1-5:30s", credits: 203 },
1690
+ { identifier: "omnihuman-1-5:60s", credits: 405 },
1691
+ ],
1692
+ },
1693
+ "hailuo-avatar": {
1694
+ id: "hailuo-avatar",
1695
+ kind: "video",
1696
+ modes: ["lip-sync"] as const,
1697
+ family: "MiniMax",
1698
+ label: "Hailuo Avatar",
1699
+ series: "Hailuo",
1700
+ description: "MiniMax avatar lip-sync.",
1701
+ useCases: ["lip-sync", "talking-head"],
1702
+ pricing: [{ identifier: "hailuo-avatar", credits: 19 }],
1703
+ // Not wired in any provider / not in LIP_SYNC_PROVIDERS — hide from MCP
1704
+ // list_models so the lip_sync tool can't advertise a model its route
1705
+ // rejects with a 400. Re-expose when the provider lands.
1706
+ mcpHidden: true,
1707
+ },
1708
+ "sync-lipsync-v3": {
1709
+ id: "sync-lipsync-v3",
1710
+ kind: "video",
1711
+ modes: ["lip-sync"] as const,
1712
+ family: "Sync",
1713
+ label: "Sync Lipsync v3",
1714
+ series: "Sync",
1715
+ description: "Dub existing footage — re-syncs lips to a new audio track. Video input, billed per second.",
1716
+ useCases: ["lip-sync", "dubbing", "video-to-video"],
1717
+ pricing: [
1718
+ { identifier: "sync-lipsync-v3", credits: 2000, note: "5-min ceiling (no duration given)" },
1719
+ { identifier: "sync-lipsync-v3:15s", credits: 100 },
1720
+ { identifier: "sync-lipsync-v3:30s", credits: 200 },
1721
+ { identifier: "sync-lipsync-v3:60s", credits: 400 },
1722
+ { identifier: "sync-lipsync-v3:120s", credits: 800 },
1723
+ { identifier: "sync-lipsync-v3:300s", credits: 2000, note: "5-min ceiling" },
1724
+ ],
1725
+ },
1726
+ "volcengine-lipsync": {
1727
+ id: "volcengine-lipsync",
1728
+ kind: "video",
1729
+ modes: ["lip-sync"] as const,
1730
+ family: "Volcengine",
1731
+ label: "Volcengine Lip Sync",
1732
+ series: "Volcengine",
1733
+ description: "Video-to-video AI dubbing — re-syncs lips to a new vocal track. Multi-speaker (scene detection + speaker ID) in basic mode. Video input, billed per second.",
1734
+ useCases: ["lip-sync", "dubbing", "video-to-video"],
1735
+ pricing: [
1736
+ { identifier: "volcengine-lipsync", credits: 600, note: "5-min ceiling (no duration given)" },
1737
+ { identifier: "volcengine-lipsync:15s", credits: 30 },
1738
+ { identifier: "volcengine-lipsync:30s", credits: 60 },
1739
+ { identifier: "volcengine-lipsync:60s", credits: 120 },
1740
+ { identifier: "volcengine-lipsync:120s", credits: 240 },
1741
+ { identifier: "volcengine-lipsync:300s", credits: 600, note: "5-min ceiling" },
1742
+ ],
1743
+ },
1744
+ // ── Gemini video analysis ── (Gemini-only; billed per duration bucket — the
1745
+ // structural credit formula lives in video-analysis-pricing.ts. Composite id
1746
+ // shape `video-analysis:<model>:<bucket>s` mirrors volcengine-lipsync: bare
1747
+ // row = per-model namespace root at the 10-min ceiling, plus one row per bucket.)
1748
+ "gemini-3-flash-video-analysis": {
1749
+ id: "gemini-3-flash-video-analysis",
1750
+ kind: "video",
1751
+ modes: ["video-analysis"] as const,
1752
+ family: "Google",
1753
+ label: "Video Analysis (Gemini 3 Flash)",
1754
+ series: "Gemini",
1755
+ description: "Analyze a video into a structured shot list (scenes, camera, audio) — fast Gemini tier. Billed per duration bucket.",
1756
+ useCases: ["video-analysis", "shot-list", "fast"],
1757
+ pricing: [
1758
+ { identifier: "video-analysis:gemini-3-flash", credits: 3, note: "10-min ceiling (no duration given)" },
1759
+ { identifier: "video-analysis:gemini-3-flash:60s", credits: 1 },
1760
+ { identifier: "video-analysis:gemini-3-flash:180s", credits: 1 },
1761
+ { identifier: "video-analysis:gemini-3-flash:360s", credits: 2 },
1762
+ { identifier: "video-analysis:gemini-3-flash:600s", credits: 3, note: "10-min ceiling" },
1763
+ ],
1764
+ },
1765
+ "gemini-3.1-pro-video-analysis": {
1766
+ id: "gemini-3.1-pro-video-analysis",
1767
+ kind: "video",
1768
+ modes: ["video-analysis"] as const,
1769
+ family: "Google",
1770
+ label: "Video Analysis (Gemini 3.1 Pro)",
1771
+ series: "Gemini",
1772
+ description: "Analyze a video into a structured shot list (scenes, camera, audio) — high-fidelity Gemini tier. Billed per duration bucket.",
1773
+ useCases: ["video-analysis", "shot-list", "cinematic"],
1774
+ pricing: [
1775
+ { identifier: "video-analysis:gemini-3.1-pro", credits: 11, note: "10-min ceiling (no duration given)" },
1776
+ { identifier: "video-analysis:gemini-3.1-pro:60s", credits: 2 },
1777
+ { identifier: "video-analysis:gemini-3.1-pro:180s", credits: 3 },
1778
+ { identifier: "video-analysis:gemini-3.1-pro:360s", credits: 7 },
1779
+ { identifier: "video-analysis:gemini-3.1-pro:600s", credits: 11, note: "10-min ceiling" },
1780
+ ],
1781
+ },
1782
+ }
1783
+
1784
+ // =============================================================================
1785
+ // AUDIO MODELS
1786
+ // =============================================================================
1787
+ const AUDIO_MODELS: Record<string, ModelCatalogEntry> = {
1788
+ // ── ElevenLabs TTS ──
1789
+ "elevenlabs-v3": {
1790
+ id: "elevenlabs-v3",
1791
+ kind: "audio",
1792
+ modes: ["tts"] as const,
1793
+ family: "ElevenLabs",
1794
+ label: "ElevenLabs v3",
1795
+ series: "ElevenLabs",
1796
+ description: "Latest ElevenLabs TTS — supports [audio tags] for emotion / pacing. Direct API.",
1797
+ useCases: ["tts", "voice-over", "narration", "expressive"],
1798
+ features: ["audio-tags", "voice-cloning"],
1799
+ pricing: [{ identifier: "elevenlabs-v3", credits: 3 }],
1800
+ featured: true,
1801
+ },
1802
+ "elevenlabs-turbo": {
1803
+ id: "elevenlabs-turbo",
1804
+ kind: "audio",
1805
+ modes: ["tts"] as const,
1806
+ family: "ElevenLabs",
1807
+ label: "ElevenLabs Turbo v2.5",
1808
+ series: "ElevenLabs",
1809
+ description: "Fast, cheap ElevenLabs TTS via the direct ElevenLabs API. Good for narration.",
1810
+ useCases: ["tts", "narration", "fast"],
1811
+ pricing: [{ identifier: "elevenlabs-turbo", credits: 2, note: "per 1K chars" }],
1812
+ },
1813
+ "elevenlabs-multilingual": {
1814
+ id: "elevenlabs-multilingual",
1815
+ kind: "audio",
1816
+ modes: ["tts"] as const,
1817
+ family: "ElevenLabs",
1818
+ label: "ElevenLabs Multilingual v2",
1819
+ series: "ElevenLabs",
1820
+ description: "Multi-language ElevenLabs TTS via the direct ElevenLabs API.",
1821
+ useCases: ["tts", "multilingual"],
1822
+ pricing: [{ identifier: "elevenlabs-multilingual", credits: 3, note: "per 1K chars" }],
1823
+ },
1824
+ "elevenlabs-dialogue": {
1825
+ id: "elevenlabs-dialogue",
1826
+ kind: "audio",
1827
+ modes: ["tts"] as const,
1828
+ family: "ElevenLabs",
1829
+ label: "ElevenLabs Dialogue v3",
1830
+ series: "ElevenLabs",
1831
+ description: "Multi-speaker dialogue TTS — give it a script, it voices each role.",
1832
+ useCases: ["tts", "dialogue", "multi-speaker"],
1833
+ pricing: [{ identifier: "elevenlabs-dialogue", credits: 4, note: "per 1K chars" }],
1834
+ // Driven only via the dialogue/character-voice path (multi-speaker script
1835
+ // shape), NOT the single-text generate_speech verb. Hide from MCP
1836
+ // list_models so generate_speech (TTS_PROVIDERS) can't advertise it and
1837
+ // then 400. Re-expose if a dialogue-capable MCP verb is added.
1838
+ mcpHidden: true,
1839
+ },
1840
+
1841
+ // ── ElevenLabs voice utilities ──
1842
+ "voice-clone": {
1843
+ id: "voice-clone",
1844
+ kind: "audio",
1845
+ modes: ["voice-clone"] as const,
1846
+ family: "ElevenLabs",
1847
+ label: "Voice Clone (Instant)",
1848
+ series: "ElevenLabs",
1849
+ description: "Clone a voice from a short reference clip. Instant clone via direct ElevenLabs API.",
1850
+ useCases: ["voice-clone", "personalization"],
1851
+ pricing: [{ identifier: "voice-clone", credits: 5 }],
1852
+ },
1853
+ "elevenlabs-voice-design": {
1854
+ id: "elevenlabs-voice-design",
1855
+ kind: "audio",
1856
+ modes: ["voice-design"] as const,
1857
+ family: "ElevenLabs",
1858
+ label: "ElevenLabs Voice Design",
1859
+ series: "ElevenLabs",
1860
+ description: "Design a synthetic voice from a description (no reference clip needed).",
1861
+ useCases: ["voice-design", "synthetic"],
1862
+ pricing: [{ identifier: "elevenlabs-voice-design", credits: 5 }],
1863
+ },
1864
+ "elevenlabs-voice-changer": {
1865
+ id: "elevenlabs-voice-changer",
1866
+ kind: "audio",
1867
+ modes: ["voice-changer"] as const,
1868
+ family: "ElevenLabs",
1869
+ label: "ElevenLabs Voice Changer",
1870
+ series: "ElevenLabs",
1871
+ description: "Speech-to-speech: convert one voice to another while preserving prosody.",
1872
+ useCases: ["voice-conversion", "dubbing"],
1873
+ pricing: [{ identifier: "elevenlabs-voice-changer", credits: 4 }],
1874
+ },
1875
+ "elevenlabs-stt": {
1876
+ id: "elevenlabs-stt",
1877
+ kind: "audio",
1878
+ modes: ["stt"] as const,
1879
+ family: "ElevenLabs",
1880
+ label: "ElevenLabs STT",
1881
+ series: "ElevenLabs",
1882
+ description: "Speech-to-text — transcribe audio with timestamps.",
1883
+ useCases: ["transcription", "stt"],
1884
+ pricing: [{ identifier: "elevenlabs-stt", credits: 3 }],
1885
+ },
1886
+ "elevenlabs-isolation": {
1887
+ id: "elevenlabs-isolation",
1888
+ kind: "audio",
1889
+ modes: ["isolation"] as const,
1890
+ family: "ElevenLabs",
1891
+ label: "ElevenLabs Voice Isolation",
1892
+ series: "ElevenLabs",
1893
+ description: "Strip background noise / music from a vocal track.",
1894
+ useCases: ["cleanup", "isolation"],
1895
+ pricing: [{ identifier: "elevenlabs-isolation", credits: 8, note: "variable per second" }],
1896
+ },
1897
+ "elevenlabs-dubbing": {
1898
+ id: "elevenlabs-dubbing",
1899
+ kind: "audio",
1900
+ modes: ["dubbing"] as const,
1901
+ family: "ElevenLabs",
1902
+ label: "ElevenLabs Dubbing",
1903
+ series: "ElevenLabs",
1904
+ description: "Translate + dub a video into a new language. Async.",
1905
+ useCases: ["dubbing", "multilingual"],
1906
+ pricing: [{ identifier: "elevenlabs-dubbing", credits: 8 }],
1907
+ },
1908
+ "elevenlabs-forced-alignment": {
1909
+ id: "elevenlabs-forced-alignment",
1910
+ kind: "audio",
1911
+ modes: ["forced-alignment"] as const,
1912
+ family: "ElevenLabs",
1913
+ label: "ElevenLabs Forced Alignment",
1914
+ series: "ElevenLabs",
1915
+ description: "Align an existing transcript to audio with word-level timestamps.",
1916
+ useCases: ["alignment", "captions"],
1917
+ pricing: [{ identifier: "elevenlabs-forced-alignment", credits: 3 }],
1918
+ },
1919
+
1920
+ // ── ElevenLabs SFX ──
1921
+ "elevenlabs-sfx": {
1922
+ id: "elevenlabs-sfx",
1923
+ kind: "audio",
1924
+ modes: ["sfx"] as const,
1925
+ family: "ElevenLabs",
1926
+ label: "ElevenLabs Sound Effects",
1927
+ series: "ElevenLabs",
1928
+ description: "Generate short sound effects from a text prompt.",
1929
+ useCases: ["sfx", "ambient"],
1930
+ pricing: [{ identifier: "elevenlabs-sfx", credits: 1, note: "~5s clip" }],
1931
+ },
1932
+
1933
+ // ── Suno music ──
1934
+ "suno": {
1935
+ id: "suno",
1936
+ kind: "audio",
1937
+ modes: ["music"] as const,
1938
+ family: "Suno",
1939
+ label: "Suno v4",
1940
+ series: "Suno",
1941
+ dataValue: "V4",
1942
+ description: "Suno v4 music generation — full songs with vocals, multiple genres.",
1943
+ useCases: ["music", "song", "vocals"],
1944
+ pricing: [{ identifier: "suno", credits: 3, note: "per generation" }],
1945
+ },
1946
+ "suno-v5": {
1947
+ id: "suno-v5",
1948
+ kind: "audio",
1949
+ modes: ["music"] as const,
1950
+ family: "Suno",
1951
+ label: "Suno v5",
1952
+ series: "Suno",
1953
+ dataValue: "V5",
1954
+ description: "Newer Suno v5 — better vocal quality, more genres. Same price as v4.",
1955
+ useCases: ["music", "song", "vocals", "premium"],
1956
+ pricing: [{ identifier: "suno-v5", credits: 3, note: "per generation" }],
1957
+ },
1958
+ "suno-v5_5": {
1959
+ id: "suno-v5_5",
1960
+ kind: "audio",
1961
+ modes: ["music"] as const,
1962
+ family: "Suno",
1963
+ label: "Suno v5.5",
1964
+ series: "Suno",
1965
+ dataValue: "V5_5",
1966
+ description: "Suno v5.5 — latest model with improved audio quality and expressiveness.",
1967
+ useCases: ["music", "song", "vocals", "premium"],
1968
+ pricing: [{ identifier: "suno-v5_5", credits: 3, note: "per generation" }],
1969
+ featured: true,
1970
+ },
1971
+ }
1972
+
1973
+ // =============================================================================
1974
+ // COMBINED CATALOG
1975
+ // =============================================================================
1976
+ export const MODEL_CATALOG: Record<string, ModelCatalogEntry> = {
1977
+ ...IMAGE_MODELS,
1978
+ ...VIDEO_MODELS,
1979
+ ...AUDIO_MODELS,
1980
+ }
1981
+
1982
+ /**
1983
+ * Lookup helpers for MCP and the future frontend migration.
1984
+ */
1985
+ export function listModels(filter?: {
1986
+ kind?: ModelKind
1987
+ mode?: ModelMode
1988
+ family?: string
1989
+ }): ModelCatalogEntry[] {
1990
+ const all = Object.values(MODEL_CATALOG)
1991
+ if (!filter) return all
1992
+ return all.filter((m) => {
1993
+ if (filter.kind && m.kind !== filter.kind) return false
1994
+ if (filter.mode && !m.modes.includes(filter.mode)) return false
1995
+ if (filter.family && m.family.toLowerCase() !== filter.family.toLowerCase()) return false
1996
+ return true
1997
+ })
1998
+ }
1999
+
2000
+ /**
2001
+ * Group entries by family, returning a stable order.
2002
+ * Returns Array<{ family: string; models: ModelCatalogEntry[] }>.
2003
+ */
2004
+ export function groupByFamily(
2005
+ entries: ModelCatalogEntry[],
2006
+ ): Array<{ family: string; models: ModelCatalogEntry[] }> {
2007
+ const groups = new Map<string, ModelCatalogEntry[]>()
2008
+ for (const m of entries) {
2009
+ const list = groups.get(m.family)
2010
+ if (list) list.push(m)
2011
+ else groups.set(m.family, [m])
2012
+ }
2013
+ return Array.from(groups.entries()).map(([family, models]) => ({ family, models }))
2014
+ }
2015
+
2016
+ export function getModel(id: string): ModelCatalogEntry | undefined {
2017
+ return MODEL_CATALOG[id]
2018
+ }
2019
+
2020
+ // =============================================================================
2021
+ // Per-model input validation
2022
+ // =============================================================================
2023
+ export type ValidationField =
2024
+ | "aspectRatio"
2025
+ | "resolution"
2026
+ | "quality"
2027
+ | "duration"
2028
+
2029
+ export interface ModelValidationIssue {
2030
+ field: ValidationField
2031
+ message: string
2032
+ allowed: readonly (string | number)[] | null
2033
+ }
2034
+
2035
+ /**
2036
+ * Validate that the user's lever values are supported by the chosen model.
2037
+ *
2038
+ * Returns null on success or the FIRST issue found. We surface a single issue
2039
+ * rather than collecting all so the agent doesn't get a wall of complaints —
2040
+ * fix one thing, retry. Order: aspectRatio → resolution → quality → duration.
2041
+ *
2042
+ * Semantics:
2043
+ * - If the field is undefined, it's never an issue.
2044
+ * - If the catalog entry doesn't list a corresponding lever (e.g. base
2045
+ * nano-banana has no `resolutions`), passing that field IS an issue —
2046
+ * silently dropping it has caused two bugs already.
2047
+ * - If the value is in the allow list, it's valid.
2048
+ *
2049
+ * Unknown model ids are NOT flagged here — the route handler / Zod model
2050
+ * enum is the right gate for that. We just skip validation.
2051
+ */
2052
+ export function validateModelInput(
2053
+ modelId: string,
2054
+ input: {
2055
+ aspectRatio?: string
2056
+ resolution?: string
2057
+ quality?: string
2058
+ duration?: number
2059
+ },
2060
+ ): ModelValidationIssue | null {
2061
+ const m = MODEL_CATALOG[modelId]
2062
+ if (!m) return null
2063
+
2064
+ if (input.aspectRatio !== undefined) {
2065
+ if (!m.aspectRatios) {
2066
+ return {
2067
+ field: "aspectRatio",
2068
+ message: `Model "${modelId}" does not have an aspect_ratio lever — omit it.`,
2069
+ allowed: null,
2070
+ }
2071
+ }
2072
+ if (!m.aspectRatios.includes(input.aspectRatio)) {
2073
+ return {
2074
+ field: "aspectRatio",
2075
+ message: `Model "${modelId}" does not support aspect_ratio "${input.aspectRatio}". Supported: ${m.aspectRatios.join(", ")}.`,
2076
+ allowed: m.aspectRatios,
2077
+ }
2078
+ }
2079
+ }
2080
+
2081
+ if (input.resolution !== undefined) {
2082
+ if (!m.resolutions) {
2083
+ return {
2084
+ field: "resolution",
2085
+ message: `Model "${modelId}" does not have a resolution lever — omit it.`,
2086
+ allowed: null,
2087
+ }
2088
+ }
2089
+ if (!m.resolutions.includes(input.resolution)) {
2090
+ return {
2091
+ field: "resolution",
2092
+ message: `Model "${modelId}" does not support resolution "${input.resolution}". Supported: ${m.resolutions.join(", ")}.`,
2093
+ allowed: m.resolutions,
2094
+ }
2095
+ }
2096
+ }
2097
+
2098
+ if (input.quality !== undefined) {
2099
+ if (!m.qualities) {
2100
+ return {
2101
+ field: "quality",
2102
+ message: `Model "${modelId}" does not have a quality lever — omit it.`,
2103
+ allowed: null,
2104
+ }
2105
+ }
2106
+ if (!m.qualities.includes(input.quality)) {
2107
+ return {
2108
+ field: "quality",
2109
+ message: `Model "${modelId}" does not support quality "${input.quality}". Supported: ${m.qualities.join(", ")}.`,
2110
+ allowed: m.qualities,
2111
+ }
2112
+ }
2113
+ }
2114
+
2115
+ if (input.duration !== undefined) {
2116
+ if (!m.durations) {
2117
+ return {
2118
+ field: "duration",
2119
+ message: `Model "${modelId}" does not have a duration lever — omit it.`,
2120
+ allowed: null,
2121
+ }
2122
+ }
2123
+ if (!m.durations.includes(input.duration)) {
2124
+ return {
2125
+ field: "duration",
2126
+ message: `Model "${modelId}" does not support duration ${input.duration}s. Supported: ${m.durations.join(", ")}s.`,
2127
+ allowed: m.durations,
2128
+ }
2129
+ }
2130
+ }
2131
+
2132
+ return null
2133
+ }
2134
+
2135
+ // =============================================================================
2136
+ // Frontend picker helpers — return `{value, label}[]` shapes that the
2137
+ // existing config-panel components expect, derived from the catalog so we
2138
+ // don't duplicate registries between backend and frontend.
2139
+ // =============================================================================
2140
+
2141
+ /**
2142
+ * Global value-label decorations. Per-model overrides live on
2143
+ * `ModelCatalogEntry.valueLabels` (e.g. Hailuo's "1080P (6s max)").
2144
+ *
2145
+ * Add entries here when a value should render with a richer human label.
2146
+ * Plain values (3:2, 4:3, 720p, etc.) can stay undecorated — the value
2147
+ * itself is the label and we fall back to `value` automatically.
2148
+ */
2149
+ const MODEL_VALUE_LABELS: Record<string, string> = {
2150
+ // aspect ratios
2151
+ "1:1": "1:1 (Square)",
2152
+ "16:9": "16:9 (Landscape)",
2153
+ "9:16": "9:16 (Portrait)",
2154
+ "21:9": "21:9 (Ultra-wide)",
2155
+ "9:21": "9:21 (Tall ultra-wide)",
2156
+ "auto": "Auto",
2157
+ "adaptive": "Adaptive",
2158
+ // image resolutions
2159
+ "1K": "1K (Standard)",
2160
+ "2K": "2K (High)",
2161
+ "4K": "4K (Ultra)",
2162
+ "4k": "4K",
2163
+ "8K": "8K (Ultra)",
2164
+ // qualities
2165
+ "medium": "Medium (Balanced)",
2166
+ "high": "High (Detailed)",
2167
+ "basic": "Basic (2K)",
2168
+ "TURBO": "Turbo (fast)",
2169
+ "BALANCED": "Balanced",
2170
+ "QUALITY": "Quality (best)",
2171
+ }
2172
+
2173
+ function decorateLabel(model: ModelCatalogEntry, value: string): string {
2174
+ return model.valueLabels?.[value] ?? MODEL_VALUE_LABELS[value] ?? value
2175
+ }
2176
+
2177
+ export interface LabeledOption {
2178
+ value: string
2179
+ label: string
2180
+ }
2181
+
2182
+ /** Return `{value, label}[]` for the model's aspect ratios, or `null` if none. */
2183
+ export function getAspectRatioOptions(modelId: string): LabeledOption[] | null {
2184
+ const m = MODEL_CATALOG[modelId]
2185
+ if (!m?.aspectRatios) return null
2186
+ return m.aspectRatios.map((v) => ({ value: v, label: decorateLabel(m, v) }))
2187
+ }
2188
+
2189
+ /** Return `{value, label}[]` for the model's resolution lever, or `null` if none. */
2190
+ export function getResolutionOptions(modelId: string): LabeledOption[] | null {
2191
+ const m = MODEL_CATALOG[modelId]
2192
+ if (!m?.resolutions) return null
2193
+ return m.resolutions.map((v) => ({ value: v, label: decorateLabel(m, v) }))
2194
+ }
2195
+
2196
+ /** Return `{value, label}[]` for the model's quality lever, or `null` if none. */
2197
+ export function getQualityOptions(modelId: string): LabeledOption[] | null {
2198
+ const m = MODEL_CATALOG[modelId]
2199
+ if (!m?.qualities) return null
2200
+ return m.qualities.map((v) => ({ value: v, label: decorateLabel(m, v) }))
2201
+ }
2202
+
2203
+ /** Return the model's allowed durations (in seconds), or `null` if none. */
2204
+ export function getDurationsForModel(modelId: string): number[] | null {
2205
+ const m = MODEL_CATALOG[modelId]
2206
+ if (!m?.durations) return null
2207
+ return [...m.durations]
2208
+ }
2209
+
2210
+ /**
2211
+ * Min/max credits across all pricing variants — used by the frontend's
2212
+ * "5–8 cr" range badge in the model dropdown. `null` for models with a
2213
+ * single price point.
2214
+ */
2215
+ export function getCreditRange(modelId: string): { min: number; max: number } | null {
2216
+ const m = MODEL_CATALOG[modelId]
2217
+ if (!m || m.pricing.length < 2) return null
2218
+ let min = Infinity
2219
+ let max = -Infinity
2220
+ for (const p of m.pricing) {
2221
+ if (p.credits < min) min = p.credits
2222
+ if (p.credits > max) max = p.credits
2223
+ }
2224
+ return { min, max }
2225
+ }
2226
+
2227
+ /** Whether the model declares a given capability flag in its `features` array. */
2228
+ export function hasFeature(modelId: string, feature: string): boolean {
2229
+ return MODEL_CATALOG[modelId]?.features?.includes(feature) ?? false
2230
+ }
2231
+
2232
+ /**
2233
+ * All ids in the catalog whose `features` includes the given flag. Used by
2234
+ * frontend to build PROVIDERS_WITH_END_FRAME / PROVIDERS_WITH_REFERENCES
2235
+ * lists derived from the catalog.
2236
+ */
2237
+ export function modelsWithFeature(feature: string): string[] {
2238
+ return Object.values(MODEL_CATALOG)
2239
+ .filter((m) => m.features?.includes(feature))
2240
+ .map((m) => m.id)
2241
+ }
2242
+
2243
+ /**
2244
+ * Map of `{modelId: durations[]}` for every catalog entry whose `modes`
2245
+ * includes the given mode. Used to derive KIE_VIDEO_DURATIONS (mode i2v)
2246
+ * and KIE_T2V_DURATIONS (mode t2v) on the frontend.
2247
+ */
2248
+ export function durationsByMode(mode: ModelMode): Record<string, number[]> {
2249
+ const out: Record<string, number[]> = {}
2250
+ for (const m of Object.values(MODEL_CATALOG)) {
2251
+ if (!m.modes.includes(mode)) continue
2252
+ if (!m.durations) continue
2253
+ out[m.id] = [...m.durations]
2254
+ }
2255
+ return out
2256
+ }
2257
+
2258
+ /**
2259
+ * Map of `{modelId: LabeledOption[]}` for every catalog entry of the given
2260
+ * kind that exposes a resolution lever. Used to derive
2261
+ * IMAGE_RESOLUTION_OPTIONS / VIDEO_RESOLUTION_OPTIONS on the frontend.
2262
+ */
2263
+ export function resolutionOptionsByKind(kind: ModelKind): Record<string, LabeledOption[]> {
2264
+ const out: Record<string, LabeledOption[]> = {}
2265
+ for (const m of Object.values(MODEL_CATALOG)) {
2266
+ if (m.kind !== kind) continue
2267
+ if (!m.resolutions) continue
2268
+ out[m.id] = m.resolutions.map((v) => ({ value: v, label: decorateLabel(m, v) }))
2269
+ }
2270
+ return out
2271
+ }
2272
+
2273
+ /**
2274
+ * Map of `{modelId: LabeledOption[]}` for every catalog entry of the given
2275
+ * kind that exposes an aspect_ratio lever.
2276
+ */
2277
+ export function aspectRatioOptionsByKind(kind: ModelKind): Record<string, LabeledOption[]> {
2278
+ const out: Record<string, LabeledOption[]> = {}
2279
+ for (const m of Object.values(MODEL_CATALOG)) {
2280
+ if (m.kind !== kind) continue
2281
+ if (!m.aspectRatios) continue
2282
+ out[m.id] = m.aspectRatios.map((v) => ({ value: v, label: decorateLabel(m, v) }))
2283
+ }
2284
+ return out
2285
+ }
2286
+
2287
+ /** Same shape as `aspectRatioOptionsByKind` but for `qualities`. */
2288
+ export function qualityOptionsByKind(kind: ModelKind): Record<string, LabeledOption[]> {
2289
+ const out: Record<string, LabeledOption[]> = {}
2290
+ for (const m of Object.values(MODEL_CATALOG)) {
2291
+ if (m.kind !== kind) continue
2292
+ if (!m.qualities) continue
2293
+ out[m.id] = m.qualities.map((v) => ({ value: v, label: decorateLabel(m, v) }))
2294
+ }
2295
+ return out
2296
+ }
2297
+
2298
+ /** One shaped model row for a picker — capabilities derived from the catalog. */
2299
+ export interface ModelMenuOption {
2300
+ /** Catalog id (the API enum value). */
2301
+ id: string
2302
+ /** Human-readable display name from the catalog. */
2303
+ label: string
2304
+ /** Resolution levers, `[]` when the model exposes none. */
2305
+ resolutions: LabeledOption[]
2306
+ /** Aspect-ratio levers, `[]` when the model exposes none. */
2307
+ aspectRatios: LabeledOption[]
2308
+ /** Duration levers in seconds (union of i2v+t2v modes); `[]` for image / fixed-duration models. */
2309
+ durations: { value: number; label: string }[]
2310
+ }
2311
+
2312
+ /**
2313
+ * Build shaped picker rows for one catalog `kind` from a caller-supplied id
2314
+ * allowlist (the curation — which ids, in what order, stays the caller's). Every
2315
+ * CAPABILITY is derived from the catalog accessors, never hardcoded, so the menu
2316
+ * can't drift as the catalog evolves. Ids absent from the catalog or of the
2317
+ * wrong kind are skipped (so a stale allowlist entry drops out cleanly). Pure;
2318
+ * returns fresh arrays (never aliases the accessor maps).
2319
+ */
2320
+ export function buildModelMenu(
2321
+ kind: ModelKind,
2322
+ allowlist: readonly string[],
2323
+ ): ModelMenuOption[] {
2324
+ const resByModel = resolutionOptionsByKind(kind)
2325
+ const arByModel = aspectRatioOptionsByKind(kind)
2326
+ const i2v = durationsByMode("i2v")
2327
+ const t2v = durationsByMode("t2v")
2328
+ const out: ModelMenuOption[] = []
2329
+ for (const id of allowlist) {
2330
+ const entry = MODEL_CATALOG[id]
2331
+ if (!entry || entry.kind !== kind) continue
2332
+ const durations = Array.from(new Set([...(i2v[id] ?? []), ...(t2v[id] ?? [])]))
2333
+ .sort((a, b) => a - b)
2334
+ .map((n) => ({ value: n, label: `${n}s` }))
2335
+ out.push({
2336
+ id: entry.id,
2337
+ label: entry.label,
2338
+ resolutions: [...(resByModel[id] ?? [])],
2339
+ aspectRatios: [...(arByModel[id] ?? [])],
2340
+ durations,
2341
+ })
2342
+ }
2343
+ return out
2344
+ }
2345
+
2346
+ /** All `{modelId: {min, max}}` entries that have variable pricing (>1 variant). */
2347
+ export function creditRangesAll(): Record<string, { min: number; max: number }> {
2348
+ const out: Record<string, { min: number; max: number }> = {}
2349
+ for (const m of Object.values(MODEL_CATALOG)) {
2350
+ const range = getCreditRange(m.id)
2351
+ if (range) out[m.id] = range
2352
+ }
2353
+ return out
2354
+ }
2355
+
2356
+ /**
2357
+ * Tuple of model ids for a given kind + one or more modes. Built for use as
2358
+ * a Zod `z.enum()` argument so MCP tool input schemas stay in sync with the
2359
+ * catalog without manual maintenance.
2360
+ *
2361
+ * Filters out `mcpHidden: true` entries by default — pass `includeHidden`
2362
+ * to surface legacy versions (used internally by validators that still need
2363
+ * to recognize older ids forwarded by direct API callers).
2364
+ *
2365
+ * Pass `kind: null` to skip the kind filter — useful for cross-kind ids
2366
+ * like "grok" (catalog kind=image, modes include both t2i and t2v).
2367
+ */
2368
+ export function modelIdsByKindMode(
2369
+ kind: ModelKind | null,
2370
+ modes: readonly ModelMode[],
2371
+ opts: { includeHidden?: boolean } = {},
2372
+ ): [string, ...string[]] {
2373
+ const includeHidden = opts.includeHidden === true
2374
+ const ids = Object.values(MODEL_CATALOG)
2375
+ .filter((m) => (kind === null ? true : m.kind === kind))
2376
+ .filter((m) => m.modes.some((md) => modes.includes(md)))
2377
+ .filter((m) => includeHidden || !m.mcpHidden)
2378
+ .map((m) => m.id)
2379
+ .sort()
2380
+ if (ids.length === 0) {
2381
+ throw new Error(
2382
+ `modelIdsByKindMode: no models match kind=${kind ?? "*"} modes=[${modes.join(",")}]`,
2383
+ )
2384
+ }
2385
+ return ids as [string, ...string[]]
2386
+ }