@mevdragon/vidfarm-devcli 0.2.13 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/README.md +11 -4
  2. package/SKILL.director.md +97 -0
  3. package/SKILL.platform.md +66 -6
  4. package/dist/src/cli.js +137 -1524
  5. package/dist/src/dev-serve.js +169 -0
  6. package/package.json +5 -24
  7. package/.env.example +0 -60
  8. package/auto-create-hyperframe-templates/AUTO_CREATE_HYPERFRAME_TEMPLATES.md +0 -275
  9. package/auto-create-hyperframe-templates/extractor-system-prompt.md +0 -128
  10. package/auto-create-hyperframe-templates/input-manifest.schema.json +0 -56
  11. package/auto-create-hyperframe-templates/lambda-memory-estimator.md +0 -118
  12. package/auto-create-hyperframe-templates/production-graph.schema.json +0 -314
  13. package/auto-create-hyperframe-templates/runbook.md +0 -198
  14. package/auto-create-hyperframe-templates/scripts/create-hyperframe-template.mjs +0 -368
  15. package/auto-create-hyperframe-templates/template-plan.schema.json +0 -311
  16. package/auto-create-hyperframe-templates/template-planner-prompt.md +0 -144
  17. package/auto-create-templates/AUTO_CREATE_TEMPLATES.md +0 -599
  18. package/auto-create-templates/extractor-system-prompt.md +0 -119
  19. package/auto-create-templates/production-graph.schema.json +0 -276
  20. package/auto-create-templates/runbook.md +0 -140
  21. package/auto-create-templates/template-plan.schema.json +0 -301
  22. package/auto-create-templates/template-planner-prompt.md +0 -137
  23. package/dist/src/account-pages-legacy.js +0 -9336
  24. package/dist/src/account-pages-legacy.js.map +0 -1
  25. package/dist/src/account-pages.js +0 -61
  26. package/dist/src/account-pages.js.map +0 -1
  27. package/dist/src/app.js +0 -14023
  28. package/dist/src/app.js.map +0 -1
  29. package/dist/src/cli.js.map +0 -1
  30. package/dist/src/composition-runtime.js +0 -581
  31. package/dist/src/composition-runtime.js.map +0 -1
  32. package/dist/src/config.js +0 -140
  33. package/dist/src/config.js.map +0 -1
  34. package/dist/src/context.js +0 -492
  35. package/dist/src/context.js.map +0 -1
  36. package/dist/src/dev-app-legacy.js +0 -694
  37. package/dist/src/dev-app-legacy.js.map +0 -1
  38. package/dist/src/dev-app.js +0 -6
  39. package/dist/src/dev-app.js.map +0 -1
  40. package/dist/src/domain.js +0 -2
  41. package/dist/src/domain.js.map +0 -1
  42. package/dist/src/editor-chat-history.js +0 -82
  43. package/dist/src/editor-chat-history.js.map +0 -1
  44. package/dist/src/editor-chat.js +0 -311
  45. package/dist/src/editor-chat.js.map +0 -1
  46. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  47. package/dist/src/frontend/flockposter-cache-store.js.map +0 -1
  48. package/dist/src/frontend/homepage-client.js +0 -117
  49. package/dist/src/frontend/homepage-client.js.map +0 -1
  50. package/dist/src/frontend/homepage-shared.js +0 -4
  51. package/dist/src/frontend/homepage-shared.js.map +0 -1
  52. package/dist/src/frontend/homepage-store.js +0 -28
  53. package/dist/src/frontend/homepage-store.js.map +0 -1
  54. package/dist/src/frontend/homepage-view.js +0 -260
  55. package/dist/src/frontend/homepage-view.js.map +0 -1
  56. package/dist/src/frontend/page-runtime-client.js +0 -103
  57. package/dist/src/frontend/page-runtime-client.js.map +0 -1
  58. package/dist/src/frontend/page-runtime-store.js +0 -9
  59. package/dist/src/frontend/page-runtime-store.js.map +0 -1
  60. package/dist/src/frontend/sentry.js +0 -42
  61. package/dist/src/frontend/sentry.js.map +0 -1
  62. package/dist/src/frontend/template-editor-chat.js +0 -3767
  63. package/dist/src/frontend/template-editor-chat.js.map +0 -1
  64. package/dist/src/help-page.js +0 -333
  65. package/dist/src/help-page.js.map +0 -1
  66. package/dist/src/homepage.js +0 -919
  67. package/dist/src/homepage.js.map +0 -1
  68. package/dist/src/hyperframes/composition.js +0 -180
  69. package/dist/src/hyperframes/composition.js.map +0 -1
  70. package/dist/src/index.js +0 -16
  71. package/dist/src/index.js.map +0 -1
  72. package/dist/src/instrument.js +0 -30
  73. package/dist/src/instrument.js.map +0 -1
  74. package/dist/src/lib/crypto.js +0 -45
  75. package/dist/src/lib/crypto.js.map +0 -1
  76. package/dist/src/lib/dev-log.js +0 -54
  77. package/dist/src/lib/dev-log.js.map +0 -1
  78. package/dist/src/lib/display-name.js +0 -11
  79. package/dist/src/lib/display-name.js.map +0 -1
  80. package/dist/src/lib/ids.js +0 -24
  81. package/dist/src/lib/ids.js.map +0 -1
  82. package/dist/src/lib/images.js +0 -19
  83. package/dist/src/lib/images.js.map +0 -1
  84. package/dist/src/lib/json.js +0 -15
  85. package/dist/src/lib/json.js.map +0 -1
  86. package/dist/src/lib/template-dna.js +0 -543
  87. package/dist/src/lib/template-dna.js.map +0 -1
  88. package/dist/src/lib/template-paths.js +0 -28
  89. package/dist/src/lib/template-paths.js.map +0 -1
  90. package/dist/src/lib/template-style-options.js +0 -203
  91. package/dist/src/lib/template-style-options.js.map +0 -1
  92. package/dist/src/lib/time.js +0 -7
  93. package/dist/src/lib/time.js.map +0 -1
  94. package/dist/src/lib/video-quality-harness.js +0 -60
  95. package/dist/src/lib/video-quality-harness.js.map +0 -1
  96. package/dist/src/page-runtime.js +0 -2
  97. package/dist/src/page-runtime.js.map +0 -1
  98. package/dist/src/page-shell.js +0 -2393
  99. package/dist/src/page-shell.js.map +0 -1
  100. package/dist/src/primitive-context.js +0 -299
  101. package/dist/src/primitive-context.js.map +0 -1
  102. package/dist/src/primitive-registry.js +0 -2181
  103. package/dist/src/primitive-registry.js.map +0 -1
  104. package/dist/src/primitive-sdk.js +0 -4
  105. package/dist/src/primitive-sdk.js.map +0 -1
  106. package/dist/src/primitives/remotion/html-image.js +0 -30
  107. package/dist/src/primitives/remotion/html-image.js.map +0 -1
  108. package/dist/src/primitives/remotion/media-slideshow.js +0 -60
  109. package/dist/src/primitives/remotion/media-slideshow.js.map +0 -1
  110. package/dist/src/react-page-shell.js +0 -35
  111. package/dist/src/react-page-shell.js.map +0 -1
  112. package/dist/src/ready-post-schedule-component.js +0 -1515
  113. package/dist/src/ready-post-schedule-component.js.map +0 -1
  114. package/dist/src/registry.js +0 -296
  115. package/dist/src/registry.js.map +0 -1
  116. package/dist/src/runtime.js +0 -31
  117. package/dist/src/runtime.js.map +0 -1
  118. package/dist/src/services/api-call-history.js +0 -249
  119. package/dist/src/services/api-call-history.js.map +0 -1
  120. package/dist/src/services/auth.js +0 -152
  121. package/dist/src/services/auth.js.map +0 -1
  122. package/dist/src/services/billing.js +0 -231
  123. package/dist/src/services/billing.js.map +0 -1
  124. package/dist/src/services/chat-threads.js +0 -92
  125. package/dist/src/services/chat-threads.js.map +0 -1
  126. package/dist/src/services/fork-access.js +0 -94
  127. package/dist/src/services/fork-access.js.map +0 -1
  128. package/dist/src/services/ghostcut.js +0 -132
  129. package/dist/src/services/ghostcut.js.map +0 -1
  130. package/dist/src/services/hyperframes.js +0 -1014
  131. package/dist/src/services/hyperframes.js.map +0 -1
  132. package/dist/src/services/job-capacity.js +0 -14
  133. package/dist/src/services/job-capacity.js.map +0 -1
  134. package/dist/src/services/job-logs.js +0 -197
  135. package/dist/src/services/job-logs.js.map +0 -1
  136. package/dist/src/services/jobs.js +0 -136
  137. package/dist/src/services/jobs.js.map +0 -1
  138. package/dist/src/services/media-processing.js +0 -743
  139. package/dist/src/services/media-processing.js.map +0 -1
  140. package/dist/src/services/primitive-media-lambda.js +0 -280
  141. package/dist/src/services/primitive-media-lambda.js.map +0 -1
  142. package/dist/src/services/providers.js +0 -2870
  143. package/dist/src/services/providers.js.map +0 -1
  144. package/dist/src/services/rate-limits.js +0 -237
  145. package/dist/src/services/rate-limits.js.map +0 -1
  146. package/dist/src/services/remotion.js +0 -688
  147. package/dist/src/services/remotion.js.map +0 -1
  148. package/dist/src/services/serverless-auth.js +0 -374
  149. package/dist/src/services/serverless-auth.js.map +0 -1
  150. package/dist/src/services/serverless-jobs.js +0 -1074
  151. package/dist/src/services/serverless-jobs.js.map +0 -1
  152. package/dist/src/services/serverless-provider-keys.js +0 -401
  153. package/dist/src/services/serverless-provider-keys.js.map +0 -1
  154. package/dist/src/services/serverless-records.js +0 -1086
  155. package/dist/src/services/serverless-records.js.map +0 -1
  156. package/dist/src/services/serverless-template-configs.js +0 -67
  157. package/dist/src/services/serverless-template-configs.js.map +0 -1
  158. package/dist/src/services/storage.js +0 -382
  159. package/dist/src/services/storage.js.map +0 -1
  160. package/dist/src/services/template-certification.js +0 -475
  161. package/dist/src/services/template-certification.js.map +0 -1
  162. package/dist/src/services/template-loader.js +0 -99
  163. package/dist/src/services/template-loader.js.map +0 -1
  164. package/dist/src/services/template-runtime-bundles.js +0 -217
  165. package/dist/src/services/template-runtime-bundles.js.map +0 -1
  166. package/dist/src/services/template-sources.js +0 -1017
  167. package/dist/src/services/template-sources.js.map +0 -1
  168. package/dist/src/services/video-normalization.js +0 -2
  169. package/dist/src/services/video-normalization.js.map +0 -1
  170. package/dist/src/services/webhooks.js +0 -62
  171. package/dist/src/services/webhooks.js.map +0 -1
  172. package/dist/src/template-editor-pages.js +0 -6624
  173. package/dist/src/template-editor-pages.js.map +0 -1
  174. package/dist/src/template-editor-shell.js +0 -2713
  175. package/dist/src/template-editor-shell.js.map +0 -1
  176. package/dist/src/template-sdk.js +0 -4
  177. package/dist/src/template-sdk.js.map +0 -1
  178. package/dist/src/worker.js +0 -17
  179. package/dist/src/worker.js.map +0 -1
  180. package/public/assets/homepage-app.js +0 -54
  181. package/public/assets/homepage-client-app.js +0 -80
  182. package/public/assets/page-runtime-client-app.js +0 -94
  183. package/readme.secret.md +0 -89
  184. package/templates/vidfarm_template_0000/README.md +0 -106
  185. package/templates/vidfarm_template_0000/SKILL.md +0 -266
  186. package/templates/vidfarm_template_0000/assets/Abel-Regular.ttf +0 -0
  187. package/templates/vidfarm_template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
  188. package/templates/vidfarm_template_0000/assets/Montserrat[wght].ttf +0 -0
  189. package/templates/vidfarm_template_0000/assets/SourceCodePro[wght].ttf +0 -0
  190. package/templates/vidfarm_template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
  191. package/templates/vidfarm_template_0000/assets/Yesteryear-Regular.ttf +0 -0
  192. package/templates/vidfarm_template_0000/composition.json +0 -11
  193. package/templates/vidfarm_template_0000/src/lib/images.js +0 -221
  194. package/templates/vidfarm_template_0000/src/remotion/Root.js +0 -33
  195. package/templates/vidfarm_template_0000/src/remotion/index.js +0 -3
  196. package/templates/vidfarm_template_0000/src/sdk.js +0 -3
  197. package/templates/vidfarm_template_0000/src/style-options.js +0 -200
  198. package/templates/vidfarm_template_0000/src/template-dna.js +0 -9
  199. package/templates/vidfarm_template_0000/src/template.js +0 -1566
  200. package/templates/vidfarm_template_0000/template.config.json +0 -21
@@ -1,2870 +0,0 @@
1
- import { config } from "../config.js";
2
- import { parseJson } from "../lib/json.js";
3
- import { decryptString } from "../lib/crypto.js";
4
- import { createId } from "../lib/ids.js";
5
- import { addSeconds } from "../lib/time.js";
6
- import { ServerlessProviderKeyService } from "./serverless-provider-keys.js";
7
- import sharp from "sharp";
8
- export class ProviderKeyUnavailableError extends Error {
9
- }
10
- export class ProviderAuthError extends Error {
11
- }
12
- export class ProviderQuotaExceededError extends Error {
13
- }
14
- export class ProviderWaitExceededError extends Error {
15
- }
16
- export class ProviderLeaseTimeoutError extends Error {
17
- retryAfterSeconds;
18
- constructor(message, retryAfterSeconds = 5) {
19
- super(message);
20
- this.retryAfterSeconds = retryAfterSeconds;
21
- }
22
- }
23
- export class ProviderRateLimitError extends Error {
24
- retryAfterSeconds;
25
- constructor(message, retryAfterSeconds = 60) {
26
- super(message);
27
- this.retryAfterSeconds = retryAfterSeconds;
28
- }
29
- }
30
- export class ProviderTransientError extends Error {
31
- retryAfterSeconds;
32
- constructor(message, retryAfterSeconds = 5) {
33
- super(message);
34
- this.retryAfterSeconds = retryAfterSeconds;
35
- }
36
- }
37
- const SUPPORTED_TEXT_MODELS = {
38
- openai: new Set(["gpt-5.4"]),
39
- gemini: new Set(["gemini-3.1-flash-lite", "gemini-3.1-flash-lite-preview", "gemini-2.5-flash-lite"]),
40
- openrouter: new Set(["qwen/qwen3.6-plus"]),
41
- perplexity: new Set(["sonar"])
42
- };
43
- const SUPPORTED_IMAGE_MODELS = {
44
- openai: new Set(["gpt-image-2", "gpt-image-1"]),
45
- gemini: new Set(["gemini-3-pro-image-preview", "gemini-3.1-flash-image-preview", "gemini-2.5-flash-image"]),
46
- openrouter: new Set([
47
- "google/gemini-3.1-flash-image-preview",
48
- "google/gemini-2.5-flash-image-preview",
49
- "bytedance/seedance-2.0",
50
- "bytedance-seed/seedream-4.5"
51
- ])
52
- };
53
- const SUPPORTED_VIDEO_MODELS = {
54
- openai: new Set(["sora-2", "sora-2-pro"]),
55
- gemini: new Set(["veo-3.0-generate-001", "veo-3.0-fast-generate-001"]),
56
- openrouter: new Set([
57
- "bytedance/seedance-2.0-fast",
58
- "bytedance/seedance-2.0",
59
- "google/veo-3.1-lite",
60
- "google/veo-3.1-fast",
61
- "google/veo-3.1",
62
- "kwaivgi/kling-v3.0-std",
63
- "kwaivgi/kling-v3.0-pro",
64
- "openai/sora-2-pro"
65
- ])
66
- };
67
- const DEFAULT_TEXT_MODELS = {
68
- openai: "gpt-5.4",
69
- gemini: "gemini-3.1-flash-lite",
70
- openrouter: "qwen/qwen3.6-plus",
71
- perplexity: "sonar"
72
- };
73
- const DEFAULT_IMAGE_MODELS = {
74
- openai: "gpt-image-2",
75
- gemini: "gemini-3-pro-image-preview",
76
- openrouter: "google/gemini-3.1-flash-image-preview"
77
- };
78
- const DEFAULT_VIDEO_MODELS = {
79
- openai: "sora-2",
80
- gemini: "veo-3.0-generate-001",
81
- openrouter: "bytedance/seedance-2.0"
82
- };
83
- const PROVIDER_FALLBACK_PRIORITY = ["openai", "gemini", "openrouter", "perplexity"];
84
- const OPENAI_IMAGE_SIZES = new Set([
85
- "1024x1024",
86
- "1536x1024",
87
- "1024x1536",
88
- "2048x2048",
89
- "2048x1152",
90
- "3840x2160",
91
- "2160x3840",
92
- "auto"
93
- ]);
94
- const GEMINI_IMAGE_ASPECT_RATIOS = new Set(["1:1", "16:9", "9:16", "4:3", "3:4", "21:9", "9:21"]);
95
- const SUPPORTED_TTS_MODELS = {
96
- openrouter: new Set(["google/gemini-3.1-flash-tts-preview"]),
97
- gemini: new Set(["gemini-3.1-flash-tts-preview", "gemini-2.5-flash-preview-tts"]),
98
- openai: new Set(["gpt-4o-mini-tts-2025-12-15"])
99
- };
100
- const SUPPORTED_STT_MODELS = {
101
- openrouter: new Set(["openai/gpt-4o-mini-transcribe"]),
102
- gemini: new Set(["gemini-3.1-flash-lite-preview", "gemini-2.5-flash-lite"]),
103
- openai: new Set(["gpt-4o-mini-transcribe-2025-12-15"])
104
- };
105
- const PROVIDER_KEY_VALIDATION_MODELS = {
106
- openai: "gpt-4.1-mini",
107
- gemini: "gemini-3.1-flash-lite-preview",
108
- openrouter: "google/gemini-3.1-flash-lite-preview",
109
- perplexity: "sonar"
110
- };
111
- function readStoredProviderSecret(value) {
112
- try {
113
- return decryptString(value, config.ENCRYPTION_SECRET);
114
- }
115
- catch {
116
- return value;
117
- }
118
- }
119
- function sleep(ms) {
120
- return new Promise((resolve) => setTimeout(resolve, ms));
121
- }
122
- function formatDurationSeconds(totalSeconds) {
123
- if (totalSeconds >= 3600) {
124
- const hours = Math.round(totalSeconds / 3600);
125
- return `${hours} hour${hours === 1 ? "" : "s"}`;
126
- }
127
- if (totalSeconds >= 60) {
128
- const minutes = Math.round(totalSeconds / 60);
129
- return `${minutes} minute${minutes === 1 ? "" : "s"}`;
130
- }
131
- return `${Math.max(1, Math.round(totalSeconds))} second${Math.round(totalSeconds) === 1 ? "" : "s"}`;
132
- }
133
- function parseRetryAfterSeconds(value) {
134
- if (!value) {
135
- return 60;
136
- }
137
- const numeric = Number(value);
138
- if (Number.isFinite(numeric) && numeric > 0) {
139
- return Math.max(1, Math.min(Math.ceil(numeric), 60 * 60));
140
- }
141
- const parsedDate = Date.parse(value);
142
- if (Number.isFinite(parsedDate)) {
143
- return Math.max(1, Math.min(Math.ceil((parsedDate - Date.now()) / 1000), 60 * 60));
144
- }
145
- return 60;
146
- }
147
- function isHardQuotaError(details) {
148
- const normalized = details.toLowerCase();
149
- return [
150
- "insufficient_quota",
151
- "quota_exceeded",
152
- "exceeded your current quota",
153
- "billing",
154
- "insufficient credits",
155
- "not enough credits",
156
- "credit balance",
157
- "prepaid credits",
158
- "resource_exhausted"
159
- ].some((pattern) => normalized.includes(pattern));
160
- }
161
- function conciseProviderDetails(details) {
162
- const compact = details.replace(/\s+/g, " ").trim();
163
- return compact ? `: ${compact.slice(0, 500)}` : "";
164
- }
165
- function conciseProviderResponse(operation, response, details) {
166
- return `${operation} returned ${response.status}${conciseProviderDetails(details)}`;
167
- }
168
- async function rateLimitOrQuotaError(provider, response) {
169
- const details = await response.text();
170
- if (isHardQuotaError(details)) {
171
- return new ProviderQuotaExceededError(`${provider} quota or billing limit was reached${conciseProviderDetails(details)}`);
172
- }
173
- return new ProviderRateLimitError(`${provider} rate limited${conciseProviderDetails(details)}`, parseRetryAfterSeconds(response.headers.get("retry-after")));
174
- }
175
- function isTransientProviderStatus(status) {
176
- return status === 408 || status === 409 || status === 425 || status === 500 || status === 502 || status === 503 || status === 504;
177
- }
178
- function isTransientRemoteMediaStatus(status) {
179
- return status === 408 || status === 425 || status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
180
- }
181
- function remoteMediaRetryDelayMs(response, attempt) {
182
- const retryAfter = response?.headers.get("retry-after") ?? null;
183
- if (retryAfter) {
184
- return Math.min(parseRetryAfterSeconds(retryAfter) * 1000, 5_000);
185
- }
186
- return Math.min(250 * (2 ** attempt), 2_000);
187
- }
188
- async function transientProviderError(provider, operation, response) {
189
- const details = await response.text().catch(() => "");
190
- return new ProviderTransientError(`${provider} ${operation} temporarily failed with ${response.status}${conciseProviderDetails(details)}`, parseRetryAfterSeconds(response.headers.get("retry-after")));
191
- }
192
- async function waitForRetryableProviderRateLimit(error, deadlineMs) {
193
- const waitMs = error.retryAfterSeconds * 1000;
194
- if (Date.now() + waitMs > deadlineMs) {
195
- throw new ProviderWaitExceededError(`Provider remained rate limited for ${formatDurationSeconds(config.PROVIDER_LEASE_INLINE_WAIT_SECONDS)}. Last error: ${error.message}`);
196
- }
197
- await sleep(waitMs);
198
- }
199
- async function waitForRetryableProviderError(error, deadlineMs) {
200
- if (error instanceof ProviderQuotaExceededError || error instanceof ProviderAuthError) {
201
- return false;
202
- }
203
- if (error instanceof ProviderRateLimitError) {
204
- await waitForRetryableProviderRateLimit(error, deadlineMs);
205
- return true;
206
- }
207
- if (error instanceof ProviderLeaseTimeoutError) {
208
- const waitMs = error.retryAfterSeconds * 1000;
209
- if (Date.now() + waitMs > deadlineMs) {
210
- throw new ProviderWaitExceededError(`No eligible provider key became available within ${formatDurationSeconds(config.PROVIDER_LEASE_INLINE_WAIT_SECONDS)}. Last error: ${error.message}`);
211
- }
212
- await sleep(waitMs);
213
- return true;
214
- }
215
- if (error instanceof ProviderTransientError) {
216
- const waitMs = error.retryAfterSeconds * 1000;
217
- if (Date.now() + waitMs > deadlineMs) {
218
- throw new ProviderWaitExceededError(`Provider remained temporarily unavailable within ${formatDurationSeconds(config.PROVIDER_LEASE_INLINE_WAIT_SECONDS)}. Last error: ${error.message}`);
219
- }
220
- await sleep(waitMs);
221
- return true;
222
- }
223
- return false;
224
- }
225
- function isRecoverableProviderAttemptError(error) {
226
- return error instanceof ProviderLeaseTimeoutError ||
227
- error instanceof ProviderKeyUnavailableError ||
228
- error instanceof ProviderRateLimitError ||
229
- error instanceof ProviderQuotaExceededError ||
230
- error instanceof ProviderAuthError ||
231
- error instanceof ProviderTransientError;
232
- }
233
- function zeroUsageCost(result) {
234
- return {
235
- ...result,
236
- usage: {
237
- ...result.usage,
238
- costUsd: 0
239
- }
240
- };
241
- }
242
- function providerSupportsModel(kind, provider, model) {
243
- const supported = kind === "text" ? SUPPORTED_TEXT_MODELS : kind === "image" ? SUPPORTED_IMAGE_MODELS : SUPPORTED_VIDEO_MODELS;
244
- return providerSupportsModelFrom(supported, provider, model);
245
- }
246
- function defaultModelFor(kind, provider) {
247
- return kind === "text" ? DEFAULT_TEXT_MODELS[provider] : kind === "image" ? DEFAULT_IMAGE_MODELS[provider] : DEFAULT_VIDEO_MODELS[provider];
248
- }
249
- function providerSupportsModelFrom(supported, provider, model) {
250
- return Boolean(supported[provider]?.has(normalizeProviderModel(model)));
251
- }
252
- function defaultSpeechModelFor(kind, provider) {
253
- return kind === "tts" ? SUPPORTED_TTS_MODELS[provider]?.values().next().value : SUPPORTED_STT_MODELS[provider]?.values().next().value;
254
- }
255
- function uniqueProviders(requested, supported) {
256
- return [
257
- requested,
258
- ...PROVIDER_FALLBACK_PRIORITY.filter((provider) => provider !== requested)
259
- ].filter((provider) => supported[provider]?.size);
260
- }
261
- async function availableProviderCounts(providerKeyService, customerId) {
262
- return new Map((await providerKeyService.listAvailableProviderKeyCounts(customerId))
263
- .map((entry) => [entry.provider, entry.availableCount]));
264
- }
265
- async function configuredProviderCounts(providerKeyService, customerId) {
266
- const counts = new Map();
267
- for (const entry of await providerKeyService.listProviderKeysWithSecrets(customerId)) {
268
- const provider = String(entry.provider);
269
- const status = String(entry.status);
270
- const secret = readStoredProviderSecret(String(entry.secret ?? ""));
271
- if (status !== "active" || !secret.trim()) {
272
- continue;
273
- }
274
- counts.set(provider, (counts.get(provider) ?? 0) + 1);
275
- }
276
- return counts;
277
- }
278
- async function providerAttempts(input) {
279
- const keyCounts = await configuredProviderCounts(input.providerKeyService, input.customerId);
280
- const availableCounts = await availableProviderCounts(input.providerKeyService, input.customerId);
281
- const attempts = uniqueProviders(input.requestedProvider, input.supportedModels)
282
- .map((provider, index) => ({ provider, index }))
283
- .filter(({ provider }) => (keyCounts.get(provider) ?? 0) > 0)
284
- .filter(({ provider }) => input.allowProvider?.(provider) ?? true)
285
- .sort((a, b) => {
286
- const aAvailable = (availableCounts.get(a.provider) ?? 0) > 0 ? 0 : 1;
287
- const bAvailable = (availableCounts.get(b.provider) ?? 0) > 0 ? 0 : 1;
288
- return aAvailable - bAvailable || a.index - b.index;
289
- })
290
- .map(({ provider }) => ({
291
- provider,
292
- model: provider === input.requestedProvider && providerSupportsModelFrom(input.supportedModels, provider, input.requestedModel)
293
- ? normalizeProviderModel(input.requestedModel)
294
- : input.defaultModelForProvider(provider)
295
- }))
296
- .filter((attempt) => Boolean(attempt.model));
297
- if (!attempts.length) {
298
- const supportedProviders = Object.entries(input.supportedModels)
299
- .filter(([, models]) => models?.size)
300
- .map(([provider]) => provider)
301
- .join(", ");
302
- const configuredProviders = [...keyCounts.entries()]
303
- .filter(([, count]) => count > 0)
304
- .map(([provider]) => provider)
305
- .join(", ") || "none";
306
- throw new ProviderKeyUnavailableError(`No active ${input.capability} provider key is configured for this customer. ` +
307
- `Configured providers: ${configuredProviders}. Supported providers for this capability: ${supportedProviders || "none"}.`);
308
- }
309
- return attempts;
310
- }
311
- function normalizeOpenAIImageSize(size, aspectRatio) {
312
- if (size && OPENAI_IMAGE_SIZES.has(size)) {
313
- return size;
314
- }
315
- if (aspectRatio === "16:9") {
316
- return "1536x1024";
317
- }
318
- if (aspectRatio === "9:16") {
319
- return "1024x1536";
320
- }
321
- if (size && /^(\d+)x(\d+)$/i.test(size)) {
322
- const [, widthRaw, heightRaw] = /^(\d+)x(\d+)$/i.exec(size);
323
- const width = Number(widthRaw);
324
- const height = Number(heightRaw);
325
- if (width > height) {
326
- return "1536x1024";
327
- }
328
- if (height > width) {
329
- return "1024x1536";
330
- }
331
- }
332
- return "auto";
333
- }
334
- function normalizeGeminiAspectRatio(aspectRatio) {
335
- return aspectRatio && GEMINI_IMAGE_ASPECT_RATIOS.has(aspectRatio) ? aspectRatio : undefined;
336
- }
337
- const CHAT_PROVIDER_LEASE_PRIORITY = 0;
338
- const DEFAULT_PROVIDER_LEASE_TIMEOUT_MS = config.PROVIDER_LEASE_WAIT_TIMEOUT_SECONDS * 1000;
339
- const CHAT_PROVIDER_LEASE_TIMEOUT_MS = 15_000;
340
- const DEFAULT_PROVIDER_LEASE_SECONDS = 90;
341
- const CHAT_PROVIDER_LEASE_SECONDS = 120;
342
- const CHAT_PROVIDER_LEASE_CONCURRENCY = 2;
343
- const PROVIDER_LEASE_RENEWAL_MIN_INTERVAL_MS = 5_000;
344
- const PROVIDER_QUOTA_COOLDOWN_SECONDS = 60 * 60;
345
- function providerLeaseRetryDeadlineMs() {
346
- return Date.now() + Math.max(0, config.PROVIDER_LEASE_INLINE_WAIT_SECONDS) * 1000;
347
- }
348
- export class ProviderService {
349
- serverlessProviderKeys = new ServerlessProviderKeyService();
350
- async handleLeasedProviderError(input) {
351
- if (input.error instanceof ProviderRateLimitError) {
352
- await this.serverlessProviderKeys.recordProviderKeyUsage({
353
- id: createId("usage"),
354
- keyId: input.lease.keyId,
355
- jobId: input.jobId,
356
- provider: input.provider,
357
- model: input.model,
358
- eventType: "rate_limit",
359
- metadata: {
360
- ...(input.metadata ?? {}),
361
- retryAfterSeconds: input.error.retryAfterSeconds
362
- }
363
- });
364
- await this.serverlessProviderKeys.setProviderKeyCooldown(input.lease.keyId, addSeconds(new Date(), input.error.retryAfterSeconds));
365
- }
366
- else if (input.error instanceof ProviderQuotaExceededError) {
367
- await this.serverlessProviderKeys.recordProviderKeyUsage({
368
- id: createId("usage"),
369
- keyId: input.lease.keyId,
370
- jobId: input.jobId,
371
- provider: input.provider,
372
- model: input.model,
373
- eventType: "quota_exceeded",
374
- metadata: input.metadata ?? {}
375
- });
376
- await this.serverlessProviderKeys.setProviderKeyCooldown(input.lease.keyId, addSeconds(new Date(), PROVIDER_QUOTA_COOLDOWN_SECONDS));
377
- }
378
- else if (input.error instanceof ProviderAuthError) {
379
- await this.serverlessProviderKeys.recordProviderKeyUsage({
380
- id: createId("usage"),
381
- keyId: input.lease.keyId,
382
- jobId: input.jobId,
383
- provider: input.provider,
384
- model: input.model,
385
- eventType: "auth_error",
386
- metadata: input.metadata ?? {}
387
- });
388
- await this.serverlessProviderKeys.setProviderKeyCooldown(input.lease.keyId, null, "invalid", "auth_error");
389
- }
390
- throw input.error;
391
- }
392
- listAvailableProviders(_customerId) {
393
- return PROVIDER_FALLBACK_PRIORITY;
394
- }
395
- async listAvailableProvidersAsync(customerId) {
396
- const now = Date.now();
397
- const providers = (await this.serverlessProviderKeys.listProviderKeysWithSecrets(customerId))
398
- .map((entry) => ({
399
- provider: String(entry.provider),
400
- status: String(entry.status),
401
- cooldownUntil: entry.cooldown_until ? String(entry.cooldown_until) : null,
402
- secret: readStoredProviderSecret(String(entry.secret))
403
- }))
404
- .filter((entry) => entry.status === "active")
405
- .filter((entry) => !entry.cooldownUntil || Date.parse(entry.cooldownUntil) <= now)
406
- .filter((entry) => entry.secret.trim().length > 0)
407
- .map((entry) => entry.provider);
408
- return [...new Set(providers)];
409
- }
410
- async validateProviderKey(input) {
411
- const model = PROVIDER_KEY_VALIDATION_MODELS[input.provider];
412
- if (!model) {
413
- throw new Error(`Provider ${input.provider} does not support validation.`);
414
- }
415
- if (config.mockProviders) {
416
- return { provider: input.provider, model, mock: true };
417
- }
418
- try {
419
- await this.callProvider({
420
- provider: input.provider,
421
- model,
422
- prompt: "Reply with OK.",
423
- temperature: 0,
424
- apiKey: input.secret
425
- });
426
- return { provider: input.provider, model, mock: false };
427
- }
428
- catch (error) {
429
- throw new Error(toProviderValidationMessage(input.provider, error));
430
- }
431
- }
432
- async leaseCustomerKey(input) {
433
- const leaseToken = createId("lease");
434
- const leaseDurationSeconds = input.leaseDurationSeconds ?? DEFAULT_PROVIDER_LEASE_SECONDS;
435
- const row = await this.serverlessProviderKeys.acquireProviderKeyLease({
436
- customerId: input.customerId,
437
- provider: input.provider,
438
- leaseToken,
439
- workerId: input.workerId,
440
- jobId: input.jobId,
441
- expiresAt: addSeconds(new Date(), leaseDurationSeconds)
442
- });
443
- if (!row) {
444
- const message = `No eligible ${input.provider} key is immediately available.`;
445
- if ((input.timeoutFailure ?? "retry") === "retry") {
446
- throw new ProviderLeaseTimeoutError(message);
447
- }
448
- throw new ProviderWaitExceededError(message);
449
- }
450
- return {
451
- keyId: row.keyId,
452
- leaseToken,
453
- provider: input.provider,
454
- secret: readStoredProviderSecret(row.encryptedSecret),
455
- jobId: input.jobId,
456
- leaseDurationSeconds
457
- };
458
- }
459
- providerLeaseTimeoutMessage(waitTimeoutMs, provider) {
460
- if (waitTimeoutMs === null) {
461
- return `No eligible ${provider} key became available.`;
462
- }
463
- return `No eligible ${provider} key became available within ${formatDurationSeconds(Math.max(1, Math.floor((waitTimeoutMs ?? DEFAULT_PROVIDER_LEASE_TIMEOUT_MS) / 1000)))}.`;
464
- }
465
- throwProviderLeaseTimeout(input) {
466
- const message = this.providerLeaseTimeoutMessage(input.waitTimeoutMs, input.provider);
467
- if ((input.timeoutFailure ?? "retry") === "retry") {
468
- throw new ProviderLeaseTimeoutError(message);
469
- }
470
- throw new ProviderWaitExceededError(message);
471
- }
472
- async withLeaseRenewal(lease, run) {
473
- const renewEveryMs = Math.max(PROVIDER_LEASE_RENEWAL_MIN_INTERVAL_MS, Math.floor((lease.leaseDurationSeconds * 1000) / 2));
474
- const timer = setInterval(() => {
475
- const expiresAt = addSeconds(new Date(), lease.leaseDurationSeconds);
476
- void this.serverlessProviderKeys.extendProviderKeyLease({
477
- keyId: lease.keyId,
478
- leaseToken: lease.leaseToken,
479
- expiresAt
480
- });
481
- }, renewEveryMs);
482
- timer.unref();
483
- try {
484
- return await run();
485
- }
486
- finally {
487
- clearInterval(timer);
488
- }
489
- }
490
- async releaseLease(lease) {
491
- await this.serverlessProviderKeys.releaseProviderKeyLease({ keyId: lease.keyId, leaseToken: lease.leaseToken });
492
- }
493
- async leaseEditorChatKey(input) {
494
- const now = new Date().toISOString();
495
- const key = (await this.serverlessProviderKeys.listProviderKeysWithSecrets(input.customerId))
496
- .filter((entry) => entry.provider === input.provider)
497
- .filter((entry) => entry.status === "active")
498
- .filter((entry) => !entry.cooldown_until || entry.cooldown_until <= now)
499
- .filter((entry) => readStoredProviderSecret(entry.secret).trim().length > 0)
500
- .sort((a, b) => {
501
- const aLast = a.last_used_at ?? "";
502
- const bLast = b.last_used_at ?? "";
503
- if (!aLast && bLast)
504
- return -1;
505
- if (aLast && !bLast)
506
- return 1;
507
- return aLast.localeCompare(bLast) || a.created_at.localeCompare(b.created_at);
508
- })[0];
509
- if (!key) {
510
- throw new ProviderLeaseTimeoutError(`No eligible ${input.provider} key is currently available for editor chat.`);
511
- }
512
- return {
513
- keyId: key.id,
514
- leaseToken: createId("chatlease"),
515
- provider: input.provider,
516
- secret: readStoredProviderSecret(key.secret),
517
- jobId: input.chatId,
518
- leaseDurationSeconds: CHAT_PROVIDER_LEASE_SECONDS
519
- };
520
- }
521
- async generateText(input) {
522
- const attempts = await providerAttempts({
523
- providerKeyService: this.serverlessProviderKeys,
524
- customerId: input.customerId,
525
- capability: "text",
526
- requestedProvider: input.provider,
527
- requestedModel: input.model,
528
- supportedModels: SUPPORTED_TEXT_MODELS,
529
- defaultModelForProvider: (provider) => defaultModelFor("text", provider)
530
- });
531
- const retryDeadlineMs = providerLeaseRetryDeadlineMs();
532
- for (;;) {
533
- let lastError = null;
534
- for (const attempt of attempts) {
535
- try {
536
- return await this.generateTextWithProvider({
537
- ...input,
538
- provider: attempt.provider,
539
- model: attempt.model
540
- });
541
- }
542
- catch (error) {
543
- lastError = error;
544
- if (!isRecoverableProviderAttemptError(error)) {
545
- throw error;
546
- }
547
- }
548
- }
549
- if (await waitForRetryableProviderError(lastError, retryDeadlineMs)) {
550
- continue;
551
- }
552
- throw lastError instanceof Error ? lastError : new ProviderLeaseTimeoutError("No eligible AI provider key became available in time.");
553
- }
554
- }
555
- async generateTextWithProvider(input) {
556
- const lease = await this.leaseCustomerKey({
557
- customerId: input.customerId,
558
- provider: input.provider,
559
- jobId: input.jobId,
560
- workerId: input.workerId
561
- });
562
- try {
563
- if (config.mockProviders) {
564
- const text = [
565
- `Hook: ${input.prompt.slice(0, 80)}`,
566
- "Beat 1: Identify the pain point.",
567
- "Beat 2: Show the turning point.",
568
- "Beat 3: Close with a clear CTA."
569
- ].join("\n");
570
- await this.serverlessProviderKeys.recordProviderKeyUsage({
571
- id: createId("usage"),
572
- keyId: lease.keyId,
573
- jobId: input.jobId,
574
- provider: input.provider,
575
- model: input.model,
576
- eventType: "success",
577
- inputTokens: 180,
578
- outputTokens: 120,
579
- metadata: { mock: true }
580
- });
581
- await this.serverlessProviderKeys.touchProviderKey(lease.keyId);
582
- return {
583
- text,
584
- usage: {
585
- inputTokens: 180,
586
- outputTokens: 120,
587
- costUsd: 0
588
- }
589
- };
590
- }
591
- const result = zeroUsageCost(await this.withLeaseRenewal(lease, () => this.callProvider({ ...input, apiKey: lease.secret })));
592
- await this.serverlessProviderKeys.recordProviderKeyUsage({
593
- id: createId("usage"),
594
- keyId: lease.keyId,
595
- jobId: input.jobId,
596
- provider: input.provider,
597
- model: input.model,
598
- eventType: "success",
599
- inputTokens: result.usage.inputTokens,
600
- outputTokens: result.usage.outputTokens
601
- });
602
- await this.serverlessProviderKeys.touchProviderKey(lease.keyId);
603
- return result;
604
- }
605
- catch (error) {
606
- return await this.handleLeasedProviderError({
607
- error,
608
- lease,
609
- jobId: input.jobId,
610
- provider: input.provider,
611
- model: input.model
612
- });
613
- }
614
- finally {
615
- await this.releaseLease(lease);
616
- }
617
- }
618
- async generateSpeech(input) {
619
- const attempts = await providerAttempts({
620
- providerKeyService: this.serverlessProviderKeys,
621
- customerId: input.customerId,
622
- capability: "tts",
623
- requestedProvider: input.provider,
624
- requestedModel: input.model,
625
- supportedModels: SUPPORTED_TTS_MODELS,
626
- defaultModelForProvider: (provider) => defaultSpeechModelFor("tts", provider)
627
- });
628
- const retryDeadlineMs = providerLeaseRetryDeadlineMs();
629
- for (;;) {
630
- let lastError = null;
631
- for (const attempt of attempts) {
632
- try {
633
- return await this.generateSpeechWithProvider({
634
- ...input,
635
- provider: attempt.provider,
636
- model: attempt.model
637
- });
638
- }
639
- catch (error) {
640
- lastError = error;
641
- if (!isRecoverableProviderAttemptError(error)) {
642
- throw error;
643
- }
644
- }
645
- }
646
- if (await waitForRetryableProviderError(lastError, retryDeadlineMs)) {
647
- continue;
648
- }
649
- throw lastError instanceof Error ? lastError : new ProviderLeaseTimeoutError("No eligible AI speech provider key became available in time.");
650
- }
651
- }
652
- async generateSpeechWithProvider(input) {
653
- assertSupportedSpeechModel("tts", input.provider, input.model);
654
- const lease = await this.leaseCustomerKey({
655
- customerId: input.customerId,
656
- provider: input.provider,
657
- jobId: input.jobId,
658
- workerId: input.workerId
659
- });
660
- try {
661
- if (config.mockProviders) {
662
- const contentType = inferSpeechContentType(input.responseFormat ?? "mp3");
663
- await this.serverlessProviderKeys.recordProviderKeyUsage({
664
- id: createId("usage"),
665
- keyId: lease.keyId,
666
- jobId: input.jobId,
667
- provider: input.provider,
668
- model: input.model,
669
- eventType: "success",
670
- inputTokens: 0,
671
- outputTokens: 0,
672
- metadata: { mock: true, type: "speech_generation" }
673
- });
674
- await this.serverlessProviderKeys.touchProviderKey(lease.keyId);
675
- return {
676
- bytes: Buffer.from(`mock speech:${input.text.slice(0, 120)}`, "utf8"),
677
- contentType,
678
- usage: {
679
- inputTokens: 0,
680
- outputTokens: 0,
681
- costUsd: 0
682
- }
683
- };
684
- }
685
- const result = zeroUsageCost(await this.withLeaseRenewal(lease, () => input.provider === "gemini"
686
- ? this.callGeminiSpeechGeneration({
687
- model: input.model,
688
- text: input.text,
689
- voice: input.voice,
690
- instructions: input.instructions,
691
- apiKey: lease.secret
692
- })
693
- : this.callOpenAICompatibleSpeechGeneration({
694
- provider: input.provider === "openrouter" ? "openrouter" : "openai",
695
- model: input.model,
696
- text: input.text,
697
- voice: input.voice,
698
- instructions: input.instructions,
699
- responseFormat: input.responseFormat,
700
- apiKey: lease.secret
701
- })));
702
- await this.serverlessProviderKeys.recordProviderKeyUsage({
703
- id: createId("usage"),
704
- keyId: lease.keyId,
705
- jobId: input.jobId,
706
- provider: input.provider,
707
- model: input.model,
708
- eventType: "success",
709
- inputTokens: result.usage.inputTokens,
710
- outputTokens: result.usage.outputTokens,
711
- metadata: { type: "speech_generation" }
712
- });
713
- await this.serverlessProviderKeys.touchProviderKey(lease.keyId);
714
- return result;
715
- }
716
- catch (error) {
717
- return await this.handleLeasedProviderError({
718
- error,
719
- lease,
720
- jobId: input.jobId,
721
- provider: input.provider,
722
- model: input.model,
723
- metadata: { type: "speech_generation" }
724
- });
725
- }
726
- finally {
727
- await this.releaseLease(lease);
728
- }
729
- }
730
- async generateImage(input) {
731
- const attempts = await providerAttempts({
732
- providerKeyService: this.serverlessProviderKeys,
733
- customerId: input.customerId,
734
- capability: "image",
735
- requestedProvider: input.provider,
736
- requestedModel: input.model,
737
- supportedModels: SUPPORTED_IMAGE_MODELS,
738
- defaultModelForProvider: (provider) => defaultModelFor("image", provider),
739
- allowProvider: (provider) => provider !== "perplexity"
740
- });
741
- const retryDeadlineMs = providerLeaseRetryDeadlineMs();
742
- for (;;) {
743
- let lastError = null;
744
- for (const attempt of attempts) {
745
- try {
746
- return await this.generateImageWithProvider({
747
- ...input,
748
- provider: attempt.provider,
749
- model: attempt.model
750
- });
751
- }
752
- catch (error) {
753
- lastError = error;
754
- if (!isRecoverableProviderAttemptError(error)) {
755
- throw error;
756
- }
757
- }
758
- }
759
- if (await waitForRetryableProviderError(lastError, retryDeadlineMs)) {
760
- continue;
761
- }
762
- throw lastError instanceof Error ? lastError : new ProviderLeaseTimeoutError("No eligible AI image provider key became available in time.");
763
- }
764
- }
765
- async generateImageWithProvider(input) {
766
- const lease = await this.leaseCustomerKey({
767
- customerId: input.customerId,
768
- provider: input.provider,
769
- jobId: input.jobId,
770
- workerId: input.workerId
771
- });
772
- try {
773
- if (config.mockProviders) {
774
- const svg = buildMockSlideSvg(input.prompt);
775
- await this.serverlessProviderKeys.recordProviderKeyUsage({
776
- id: createId("usage"),
777
- keyId: lease.keyId,
778
- jobId: input.jobId,
779
- provider: input.provider,
780
- model: input.model,
781
- eventType: "success",
782
- inputTokens: 0,
783
- outputTokens: 0,
784
- metadata: { mock: true, type: "image_generation" }
785
- });
786
- await this.serverlessProviderKeys.touchProviderKey(lease.keyId);
787
- return {
788
- bytes: Buffer.from(svg, "utf8"),
789
- contentType: "image/svg+xml",
790
- revisedPrompt: input.prompt
791
- };
792
- }
793
- const result = await this.withLeaseRenewal(lease, () => (input.provider === "gemini"
794
- ? this.callGeminiImageGeneration({
795
- model: input.model,
796
- prompt: input.prompt,
797
- promptAttachments: input.promptAttachments,
798
- aspectRatio: normalizeGeminiAspectRatio(input.aspectRatio),
799
- imageSize: input.imageSize,
800
- apiKey: lease.secret
801
- })
802
- : input.provider === "openrouter"
803
- ? this.callOpenRouterImageGeneration({
804
- model: input.model,
805
- prompt: input.prompt,
806
- promptAttachments: input.promptAttachments,
807
- aspectRatio: normalizeGeminiAspectRatio(input.aspectRatio),
808
- imageSize: input.imageSize,
809
- apiKey: lease.secret
810
- })
811
- : this.callOpenAIImageGeneration({
812
- model: input.model,
813
- prompt: input.prompt,
814
- promptAttachments: input.promptAttachments,
815
- size: normalizeOpenAIImageSize(input.size, input.aspectRatio),
816
- aspectRatio: input.aspectRatio,
817
- apiKey: lease.secret
818
- })));
819
- await this.serverlessProviderKeys.recordProviderKeyUsage({
820
- id: createId("usage"),
821
- keyId: lease.keyId,
822
- jobId: input.jobId,
823
- provider: input.provider,
824
- model: input.model,
825
- eventType: "success",
826
- inputTokens: 0,
827
- outputTokens: 0,
828
- metadata: { type: "image_generation" }
829
- });
830
- await this.serverlessProviderKeys.touchProviderKey(lease.keyId);
831
- return result;
832
- }
833
- catch (error) {
834
- return await this.handleLeasedProviderError({
835
- error,
836
- lease,
837
- jobId: input.jobId,
838
- provider: input.provider,
839
- model: input.model,
840
- metadata: { type: "image_generation" }
841
- });
842
- }
843
- finally {
844
- await this.releaseLease(lease);
845
- }
846
- }
847
- async editImage(input) {
848
- const attempts = await providerAttempts({
849
- providerKeyService: this.serverlessProviderKeys,
850
- customerId: input.customerId,
851
- capability: "image",
852
- requestedProvider: input.provider,
853
- requestedModel: input.model,
854
- supportedModels: SUPPORTED_IMAGE_MODELS,
855
- defaultModelForProvider: (provider) => defaultModelFor("image", provider),
856
- allowProvider: (provider) => provider !== "perplexity"
857
- });
858
- const retryDeadlineMs = providerLeaseRetryDeadlineMs();
859
- for (;;) {
860
- let lastError = null;
861
- for (const attempt of attempts) {
862
- try {
863
- return await this.editImageWithProvider({
864
- ...input,
865
- provider: attempt.provider,
866
- model: attempt.model
867
- });
868
- }
869
- catch (error) {
870
- lastError = error;
871
- if (!isRecoverableProviderAttemptError(error)) {
872
- throw error;
873
- }
874
- }
875
- }
876
- if (await waitForRetryableProviderError(lastError, retryDeadlineMs)) {
877
- continue;
878
- }
879
- throw lastError instanceof Error ? lastError : new ProviderLeaseTimeoutError("No eligible AI image edit provider key became available in time.");
880
- }
881
- }
882
- async editImageWithProvider(input) {
883
- assertSupportedModel("image", input.provider, input.model, SUPPORTED_IMAGE_MODELS);
884
- const lease = await this.leaseCustomerKey({
885
- customerId: input.customerId,
886
- provider: input.provider,
887
- jobId: input.jobId,
888
- workerId: input.workerId
889
- });
890
- try {
891
- if (config.mockProviders) {
892
- const svg = buildMockSlideSvg(`EDIT ${input.instruction}\n${input.sourceImageUrl}`);
893
- await this.serverlessProviderKeys.recordProviderKeyUsage({
894
- id: createId("usage"),
895
- keyId: lease.keyId,
896
- jobId: input.jobId,
897
- provider: input.provider,
898
- model: input.model,
899
- eventType: "success",
900
- inputTokens: 0,
901
- outputTokens: 0,
902
- metadata: { mock: true, type: "image_edit" }
903
- });
904
- await this.serverlessProviderKeys.touchProviderKey(lease.keyId);
905
- return {
906
- bytes: Buffer.from(svg, "utf8"),
907
- contentType: "image/svg+xml",
908
- revisedPrompt: input.instruction
909
- };
910
- }
911
- const result = await this.withLeaseRenewal(lease, () => (input.provider === "openai"
912
- ? this.callOpenAIImageEdit({
913
- model: input.model,
914
- sourceImageUrl: input.sourceImageUrl,
915
- instruction: input.instruction,
916
- referenceAttachments: input.referenceAttachments,
917
- maskUrl: input.maskUrl,
918
- size: normalizeOpenAIImageSize(input.size, input.aspectRatio),
919
- apiKey: lease.secret
920
- })
921
- : input.provider === "perplexity"
922
- ? (() => {
923
- throw new Error("Perplexity image edit is not supported.");
924
- })()
925
- : this.callAttachmentDrivenImageEdit({
926
- provider: input.provider,
927
- model: input.model,
928
- sourceImageUrl: input.sourceImageUrl,
929
- instruction: input.instruction,
930
- referenceAttachments: input.referenceAttachments,
931
- maskUrl: input.maskUrl,
932
- aspectRatio: normalizeGeminiAspectRatio(input.aspectRatio),
933
- imageSize: input.imageSize,
934
- apiKey: lease.secret
935
- })));
936
- await this.serverlessProviderKeys.recordProviderKeyUsage({
937
- id: createId("usage"),
938
- keyId: lease.keyId,
939
- jobId: input.jobId,
940
- provider: input.provider,
941
- model: input.model,
942
- eventType: "success",
943
- inputTokens: 0,
944
- outputTokens: 0,
945
- metadata: { type: input.maskUrl ? "image_inpaint" : "image_edit" }
946
- });
947
- await this.serverlessProviderKeys.touchProviderKey(lease.keyId);
948
- return result;
949
- }
950
- catch (error) {
951
- return await this.handleLeasedProviderError({
952
- error,
953
- lease,
954
- jobId: input.jobId,
955
- provider: input.provider,
956
- model: input.model,
957
- metadata: { type: input.maskUrl ? "image_inpaint" : "image_edit" }
958
- });
959
- }
960
- finally {
961
- await this.releaseLease(lease);
962
- }
963
- }
964
- async inpaintImage(input) {
965
- return this.editImage({
966
- ...input,
967
- maskUrl: input.maskUrl
968
- });
969
- }
970
- async generateVideo(input) {
971
- const attempts = await providerAttempts({
972
- providerKeyService: this.serverlessProviderKeys,
973
- customerId: input.customerId,
974
- capability: "video",
975
- requestedProvider: input.provider,
976
- requestedModel: input.model,
977
- supportedModels: SUPPORTED_VIDEO_MODELS,
978
- defaultModelForProvider: (provider) => defaultModelFor("video", provider),
979
- allowProvider: (provider) => provider !== "perplexity"
980
- });
981
- const retryDeadlineMs = providerLeaseRetryDeadlineMs();
982
- for (;;) {
983
- let lastError = null;
984
- for (const attempt of attempts) {
985
- try {
986
- return await this.generateVideoWithProvider({
987
- ...input,
988
- provider: attempt.provider,
989
- model: attempt.model
990
- });
991
- }
992
- catch (error) {
993
- lastError = error;
994
- if (!isRecoverableProviderAttemptError(error)) {
995
- throw error;
996
- }
997
- }
998
- }
999
- if (await waitForRetryableProviderError(lastError, retryDeadlineMs)) {
1000
- continue;
1001
- }
1002
- throw lastError instanceof Error ? lastError : new ProviderLeaseTimeoutError("No eligible AI video provider key became available in time.");
1003
- }
1004
- }
1005
- async generateVideoWithProvider(input) {
1006
- assertSupportedModel("video", input.provider, input.model, SUPPORTED_VIDEO_MODELS);
1007
- const lease = await this.leaseCustomerKey({
1008
- customerId: input.customerId,
1009
- provider: input.provider,
1010
- jobId: input.jobId,
1011
- workerId: input.workerId,
1012
- leaseDurationSeconds: Math.max(900, DEFAULT_PROVIDER_LEASE_SECONDS)
1013
- });
1014
- try {
1015
- if (config.mockProviders) {
1016
- await this.serverlessProviderKeys.recordProviderKeyUsage({
1017
- id: createId("usage"),
1018
- keyId: lease.keyId,
1019
- jobId: input.jobId,
1020
- provider: input.provider,
1021
- model: input.model,
1022
- eventType: "success",
1023
- inputTokens: 0,
1024
- outputTokens: 0,
1025
- metadata: { mock: true, type: "video_generation" }
1026
- });
1027
- await this.serverlessProviderKeys.touchProviderKey(lease.keyId);
1028
- return {
1029
- bytes: Buffer.from(`mock video:${input.prompt.slice(0, 120)}`, "utf8"),
1030
- contentType: "video/mp4",
1031
- providerJob: { mock: true, model: input.model },
1032
- usageCostUsd: 0
1033
- };
1034
- }
1035
- const result = await this.withLeaseRenewal(lease, () => {
1036
- const request = {
1037
- model: input.model,
1038
- prompt: input.prompt,
1039
- duration: input.duration,
1040
- resolution: input.resolution,
1041
- aspectRatio: input.aspectRatio,
1042
- size: input.size,
1043
- generateAudio: input.generateAudio,
1044
- frameImages: input.frameImages,
1045
- inputReferences: input.inputReferences,
1046
- seed: input.seed,
1047
- providerOptions: input.providerOptions,
1048
- apiKey: lease.secret
1049
- };
1050
- if (input.provider === "openai") {
1051
- return this.callOpenAIVideoGeneration(request);
1052
- }
1053
- if (input.provider === "gemini") {
1054
- return this.callGeminiVideoGeneration(request);
1055
- }
1056
- if (input.provider === "openrouter") {
1057
- return this.callOpenRouterVideoGeneration(request);
1058
- }
1059
- throw new ProviderKeyUnavailableError(`AI video generation does not support ${input.provider} provider keys.`);
1060
- });
1061
- await this.serverlessProviderKeys.recordProviderKeyUsage({
1062
- id: createId("usage"),
1063
- keyId: lease.keyId,
1064
- jobId: input.jobId,
1065
- provider: input.provider,
1066
- model: input.model,
1067
- eventType: "success",
1068
- inputTokens: 0,
1069
- outputTokens: 0,
1070
- metadata: { type: "video_generation", usageCostUsd: result.usageCostUsd, providerJobId: result.providerJob.id ?? null }
1071
- });
1072
- await this.serverlessProviderKeys.touchProviderKey(lease.keyId);
1073
- return result;
1074
- }
1075
- catch (error) {
1076
- return await this.handleLeasedProviderError({
1077
- error,
1078
- lease,
1079
- jobId: input.jobId,
1080
- provider: input.provider,
1081
- model: input.model,
1082
- metadata: { type: "video_generation" }
1083
- });
1084
- }
1085
- finally {
1086
- await this.releaseLease(lease);
1087
- }
1088
- }
1089
- async callOpenAIVideoGeneration(input) {
1090
- const form = new FormData();
1091
- form.set("model", input.model);
1092
- form.set("prompt", input.prompt);
1093
- const size = normalizeOpenAIVideoSize(input.model, input.size, input.aspectRatio, input.resolution);
1094
- if (size) {
1095
- form.set("size", size);
1096
- }
1097
- const seconds = openAIVideoSeconds(input.duration);
1098
- if (seconds) {
1099
- form.set("seconds", seconds);
1100
- }
1101
- const referenceUrls = [
1102
- ...(input.frameImages ?? []).map((frame) => frame.imageUrl),
1103
- ...(input.inputReferences ?? [])
1104
- ].slice(0, 1);
1105
- for (const [index, url] of referenceUrls.entries()) {
1106
- const image = await remoteImageToOpenAIVideoBlob(url, size);
1107
- form.append("input_reference", image, openAIImageFilename(index, image.type));
1108
- }
1109
- const response = await fetch("https://api.openai.com/v1/videos", {
1110
- method: "POST",
1111
- headers: {
1112
- Authorization: `Bearer ${input.apiKey}`
1113
- },
1114
- body: form
1115
- });
1116
- if (response.status === 401 || response.status === 403) {
1117
- throw new ProviderAuthError("openai authentication failed");
1118
- }
1119
- if (response.status === 402 || response.status === 429) {
1120
- throw await rateLimitOrQuotaError("openai", response);
1121
- }
1122
- if (isTransientProviderStatus(response.status)) {
1123
- throw await transientProviderError("openai", "video generation", response);
1124
- }
1125
- if (!response.ok) {
1126
- const details = await response.text();
1127
- throw new Error(conciseProviderResponse("openai video generation", response, details));
1128
- }
1129
- const submitted = await response.json();
1130
- const videoId = readStringField(submitted, "id");
1131
- if (!videoId) {
1132
- throw new Error("openai video generation returned no video id");
1133
- }
1134
- let latestStatus = submitted;
1135
- for (let attempt = 0; attempt < 120; attempt += 1) {
1136
- if (attempt > 0) {
1137
- await sleep(30_000);
1138
- }
1139
- const statusResponse = await fetch(`https://api.openai.com/v1/videos/${encodeURIComponent(videoId)}`, {
1140
- headers: {
1141
- Authorization: `Bearer ${input.apiKey}`
1142
- }
1143
- });
1144
- if (statusResponse.status === 401 || statusResponse.status === 403) {
1145
- throw new ProviderAuthError("openai authentication failed");
1146
- }
1147
- if (statusResponse.status === 402 || statusResponse.status === 429) {
1148
- throw await rateLimitOrQuotaError("openai", statusResponse);
1149
- }
1150
- if (isTransientProviderStatus(statusResponse.status)) {
1151
- const transient = await transientProviderError("openai", "video polling", statusResponse);
1152
- await sleep(transient.retryAfterSeconds * 1000);
1153
- continue;
1154
- }
1155
- if (!statusResponse.ok) {
1156
- const details = await statusResponse.text();
1157
- throw new Error(conciseProviderResponse("openai video polling", statusResponse, details));
1158
- }
1159
- latestStatus = await statusResponse.json();
1160
- const status = String(latestStatus.status ?? "").toLowerCase();
1161
- if (status === "failed" || status === "cancelled" || status === "canceled" || status === "expired") {
1162
- const details = typeof latestStatus.error === "string" ? `: ${latestStatus.error}` : "";
1163
- throw new Error(`openai video generation ${status}${details}`);
1164
- }
1165
- if (status === "completed") {
1166
- const video = await downloadAuthenticatedVideo({
1167
- url: `https://api.openai.com/v1/videos/${encodeURIComponent(videoId)}/content`,
1168
- apiKey: input.apiKey,
1169
- provider: "openai"
1170
- });
1171
- return {
1172
- ...video,
1173
- providerJob: latestStatus,
1174
- usageCostUsd: readOpenAIUsageCost(latestStatus)
1175
- };
1176
- }
1177
- }
1178
- throw new ProviderWaitExceededError("OpenAI video generation did not complete within 60 minutes.");
1179
- }
1180
- async callGeminiVideoGeneration(input) {
1181
- const instance = {
1182
- prompt: input.prompt
1183
- };
1184
- const firstFrameUrl = input.frameImages?.find((frame) => frame.frameType === "first_frame")?.imageUrl ??
1185
- input.inputReferences?.[0];
1186
- if (firstFrameUrl) {
1187
- const { bytes, mimeType } = await fetchRemoteBytes({ url: firstFrameUrl, fallbackMimeType: "image/png" });
1188
- instance.image = {
1189
- bytesBase64Encoded: bytes.toString("base64"),
1190
- mimeType
1191
- };
1192
- }
1193
- const parameters = {
1194
- sampleCount: 1
1195
- };
1196
- const aspectRatio = geminiVideoAspectRatio(input.aspectRatio);
1197
- if (aspectRatio) {
1198
- parameters.aspectRatio = aspectRatio;
1199
- }
1200
- const resolution = geminiVideoResolution(input.resolution);
1201
- if (resolution) {
1202
- parameters.resolution = resolution;
1203
- }
1204
- const durationSeconds = geminiVideoDurationSeconds(input.duration);
1205
- if (durationSeconds) {
1206
- parameters.durationSeconds = durationSeconds;
1207
- }
1208
- if (input.generateAudio !== undefined) {
1209
- parameters.generateAudio = input.generateAudio;
1210
- }
1211
- if (input.seed !== undefined) {
1212
- parameters.seed = input.seed;
1213
- }
1214
- if (input.providerOptions) {
1215
- Object.assign(parameters, input.providerOptions);
1216
- }
1217
- const baseUrl = "https://generativelanguage.googleapis.com/v1beta";
1218
- const response = await fetch(`${baseUrl}/models/${encodeURIComponent(input.model)}:predictLongRunning`, {
1219
- method: "POST",
1220
- headers: {
1221
- "Content-Type": "application/json",
1222
- "x-goog-api-key": input.apiKey
1223
- },
1224
- body: JSON.stringify({
1225
- instances: [instance],
1226
- parameters
1227
- })
1228
- });
1229
- if (response.status === 401 || response.status === 403) {
1230
- throw new ProviderAuthError("gemini authentication failed");
1231
- }
1232
- if (response.status === 402 || response.status === 429) {
1233
- throw await rateLimitOrQuotaError("gemini", response);
1234
- }
1235
- if (!response.ok) {
1236
- const details = await response.text();
1237
- throw new Error(`gemini video generation returned ${response.status}${details ? `: ${details}` : ""}`);
1238
- }
1239
- const submitted = await response.json();
1240
- const operationName = readStringField(submitted, "name");
1241
- if (!operationName) {
1242
- throw new Error("gemini video generation returned no operation name");
1243
- }
1244
- let latestStatus = submitted;
1245
- for (let attempt = 0; attempt < 120; attempt += 1) {
1246
- if (attempt > 0) {
1247
- await sleep(30_000);
1248
- }
1249
- const statusResponse = await fetch(`${baseUrl}/${operationName}`, {
1250
- headers: {
1251
- "x-goog-api-key": input.apiKey
1252
- }
1253
- });
1254
- if (statusResponse.status === 401 || statusResponse.status === 403) {
1255
- throw new ProviderAuthError("gemini authentication failed");
1256
- }
1257
- if (statusResponse.status === 402 || statusResponse.status === 429) {
1258
- throw await rateLimitOrQuotaError("gemini", statusResponse);
1259
- }
1260
- if (!statusResponse.ok) {
1261
- const details = await statusResponse.text();
1262
- throw new Error(`gemini video polling returned ${statusResponse.status}${details ? `: ${details}` : ""}`);
1263
- }
1264
- latestStatus = await statusResponse.json();
1265
- if (latestStatus.error) {
1266
- throw new Error(`gemini video generation failed: ${JSON.stringify(latestStatus.error).slice(0, 500)}`);
1267
- }
1268
- if (latestStatus.done === true) {
1269
- const videoUrl = findGeminiVideoUri(latestStatus);
1270
- if (videoUrl) {
1271
- const video = await downloadGeminiVideo(videoUrl, input.apiKey);
1272
- return {
1273
- ...video,
1274
- providerJob: latestStatus,
1275
- usageCostUsd: 0
1276
- };
1277
- }
1278
- const inline = findGeminiInlineVideo(latestStatus);
1279
- if (inline) {
1280
- return {
1281
- bytes: inline.bytes,
1282
- contentType: inline.contentType,
1283
- providerJob: latestStatus,
1284
- usageCostUsd: 0
1285
- };
1286
- }
1287
- throw new Error("gemini video generation completed without a video URI or inline video bytes");
1288
- }
1289
- }
1290
- throw new ProviderWaitExceededError("Gemini video generation did not complete within 60 minutes.");
1291
- }
1292
- async callOpenRouterVideoGeneration(input) {
1293
- const requestBody = {
1294
- model: input.model,
1295
- prompt: input.prompt
1296
- };
1297
- if (input.aspectRatio) {
1298
- requestBody.aspect_ratio = input.aspectRatio;
1299
- }
1300
- if (input.duration !== undefined) {
1301
- requestBody.duration = input.duration;
1302
- }
1303
- if (input.resolution) {
1304
- requestBody.resolution = input.resolution;
1305
- }
1306
- if (input.size) {
1307
- requestBody.size = input.size;
1308
- }
1309
- if (input.generateAudio !== undefined) {
1310
- requestBody.generate_audio = input.generateAudio;
1311
- }
1312
- if (input.frameImages?.length) {
1313
- requestBody.frame_images = input.frameImages.map((frame) => ({
1314
- image_url: frame.imageUrl,
1315
- frame_type: frame.frameType
1316
- }));
1317
- }
1318
- if (input.inputReferences?.length) {
1319
- requestBody.input_references = input.inputReferences;
1320
- }
1321
- if (input.seed !== undefined) {
1322
- requestBody.seed = input.seed;
1323
- }
1324
- if (input.providerOptions && Object.keys(input.providerOptions).length > 0) {
1325
- requestBody.provider = input.providerOptions;
1326
- }
1327
- const response = await fetch("https://openrouter.ai/api/v1/videos", {
1328
- method: "POST",
1329
- headers: {
1330
- "Content-Type": "application/json",
1331
- Authorization: `Bearer ${input.apiKey}`
1332
- },
1333
- body: JSON.stringify(requestBody)
1334
- });
1335
- if (response.status === 401 || response.status === 403) {
1336
- throw new ProviderAuthError("openrouter authentication failed");
1337
- }
1338
- if (response.status === 402 || response.status === 429) {
1339
- throw await rateLimitOrQuotaError("openrouter", response);
1340
- }
1341
- if (!response.ok) {
1342
- const details = await response.text();
1343
- throw new Error(`openrouter video generation returned ${response.status}${details ? `: ${details}` : ""}`);
1344
- }
1345
- const submitted = await response.json();
1346
- const immediateVideoUrl = findOpenRouterVideoUrl(submitted);
1347
- if (immediateVideoUrl) {
1348
- const video = await downloadOpenRouterVideo(immediateVideoUrl, input.apiKey);
1349
- return {
1350
- ...video,
1351
- providerJob: submitted,
1352
- usageCostUsd: readOpenRouterUsageCost(submitted)
1353
- };
1354
- }
1355
- const statusUrl = normalizeOpenRouterUrl(readStringField(submitted, "polling_url") ?? readStringField(submitted, "status_url")) ??
1356
- openRouterVideoStatusUrl(readStringField(submitted, "id"));
1357
- if (!statusUrl) {
1358
- throw new Error("openrouter video generation returned no polling URL or job id");
1359
- }
1360
- let latestStatus = submitted;
1361
- for (let attempt = 0; attempt < 120; attempt += 1) {
1362
- if (attempt > 0) {
1363
- await sleep(30_000);
1364
- }
1365
- const statusResponse = await fetch(statusUrl, {
1366
- headers: {
1367
- Authorization: `Bearer ${input.apiKey}`
1368
- }
1369
- });
1370
- if (statusResponse.status === 401 || statusResponse.status === 403) {
1371
- throw new ProviderAuthError("openrouter authentication failed");
1372
- }
1373
- if (statusResponse.status === 402 || statusResponse.status === 429) {
1374
- throw await rateLimitOrQuotaError("openrouter", statusResponse);
1375
- }
1376
- if (!statusResponse.ok) {
1377
- const details = await statusResponse.text();
1378
- throw new Error(`openrouter video polling returned ${statusResponse.status}${details ? `: ${details}` : ""}`);
1379
- }
1380
- latestStatus = await statusResponse.json();
1381
- const status = String(latestStatus.status ?? "").toLowerCase();
1382
- if (status === "failed" || status === "cancelled" || status === "canceled") {
1383
- const details = typeof latestStatus.error === "string" ? `: ${latestStatus.error}` : "";
1384
- throw new Error(`openrouter video generation ${status}${details}`);
1385
- }
1386
- const videoUrl = findOpenRouterVideoUrl(latestStatus);
1387
- if (status === "completed" && videoUrl) {
1388
- const video = await downloadOpenRouterVideo(videoUrl, input.apiKey);
1389
- return {
1390
- ...video,
1391
- providerJob: latestStatus,
1392
- usageCostUsd: readOpenRouterUsageCost(latestStatus)
1393
- };
1394
- }
1395
- }
1396
- throw new ProviderWaitExceededError("OpenRouter video generation did not complete within 60 minutes.");
1397
- }
1398
- async analyzeImageLayout(input) {
1399
- const attempts = await providerAttempts({
1400
- providerKeyService: this.serverlessProviderKeys,
1401
- customerId: input.customerId,
1402
- capability: "layout_analysis",
1403
- requestedProvider: input.provider,
1404
- requestedModel: input.model,
1405
- supportedModels: SUPPORTED_TEXT_MODELS,
1406
- defaultModelForProvider: (provider) => defaultModelFor("text", provider),
1407
- allowProvider: (provider) => provider !== "perplexity"
1408
- });
1409
- const retryDeadlineMs = providerLeaseRetryDeadlineMs();
1410
- for (;;) {
1411
- let lastError = null;
1412
- for (const attempt of attempts) {
1413
- try {
1414
- return await this.analyzeImageLayoutWithProvider({
1415
- ...input,
1416
- provider: attempt.provider,
1417
- model: attempt.model
1418
- });
1419
- }
1420
- catch (error) {
1421
- lastError = error;
1422
- if (!isRecoverableProviderAttemptError(error)) {
1423
- throw error;
1424
- }
1425
- }
1426
- }
1427
- if (await waitForRetryableProviderError(lastError, retryDeadlineMs)) {
1428
- continue;
1429
- }
1430
- throw lastError instanceof Error ? lastError : new ProviderLeaseTimeoutError("No eligible AI layout-analysis provider key became available in time.");
1431
- }
1432
- }
1433
- async analyzeImageLayoutWithProvider(input) {
1434
- assertSupportedModel("layout_analysis", input.provider, input.model, SUPPORTED_TEXT_MODELS);
1435
- const lease = await this.leaseCustomerKey({
1436
- customerId: input.customerId,
1437
- provider: input.provider,
1438
- jobId: input.jobId,
1439
- workerId: input.workerId
1440
- });
1441
- try {
1442
- if (config.mockProviders) {
1443
- await this.serverlessProviderKeys.recordProviderKeyUsage({
1444
- id: createId("usage"),
1445
- keyId: lease.keyId,
1446
- jobId: input.jobId,
1447
- provider: input.provider,
1448
- model: input.model,
1449
- eventType: "success",
1450
- inputTokens: 0,
1451
- outputTokens: 0,
1452
- metadata: { mock: true, type: "layout_analysis" }
1453
- });
1454
- await this.serverlessProviderKeys.touchProviderKey(lease.keyId);
1455
- return {
1456
- zone: "bottom",
1457
- align: "center",
1458
- maxWidthPercent: 82,
1459
- justification: "Mock mode defaults to bottom-center for readable slideshow captions."
1460
- };
1461
- }
1462
- const raw = await this.withLeaseRenewal(lease, () => (input.provider === "gemini"
1463
- ? this.callGeminiLayoutAnalysis({
1464
- model: input.model,
1465
- imageUrl: input.imageUrl,
1466
- overlayText: input.overlayText,
1467
- apiKey: lease.secret
1468
- })
1469
- : this.callOpenAICompatibleLayoutAnalysis({
1470
- provider: input.provider === "openrouter" ? "openrouter" : "openai",
1471
- model: input.model,
1472
- imageUrl: input.imageUrl,
1473
- overlayText: input.overlayText,
1474
- apiKey: lease.secret
1475
- })));
1476
- const parsed = parseJson(raw, null);
1477
- if (!parsed?.zone || !parsed?.align) {
1478
- throw new Error(`${input.provider} layout analysis returned invalid JSON`);
1479
- }
1480
- await this.serverlessProviderKeys.recordProviderKeyUsage({
1481
- id: createId("usage"),
1482
- keyId: lease.keyId,
1483
- jobId: input.jobId,
1484
- provider: input.provider,
1485
- model: input.model,
1486
- eventType: "success",
1487
- inputTokens: 0,
1488
- outputTokens: 0,
1489
- metadata: { type: "layout_analysis" }
1490
- });
1491
- await this.serverlessProviderKeys.touchProviderKey(lease.keyId);
1492
- return {
1493
- zone: parsed.zone,
1494
- align: parsed.align,
1495
- maxWidthPercent: clamp(Number(parsed.maxWidthPercent ?? 76), 52, 88),
1496
- justification: parsed.justification ?? "Selected to preserve legibility and subject framing."
1497
- };
1498
- }
1499
- catch (error) {
1500
- return await this.handleLeasedProviderError({
1501
- error,
1502
- lease,
1503
- jobId: input.jobId,
1504
- provider: input.provider,
1505
- model: input.model,
1506
- metadata: { type: "layout_analysis" }
1507
- });
1508
- }
1509
- finally {
1510
- await this.releaseLease(lease);
1511
- }
1512
- }
1513
- async transcribeSpeech(input) {
1514
- const attempts = await providerAttempts({
1515
- providerKeyService: this.serverlessProviderKeys,
1516
- customerId: input.customerId,
1517
- capability: "stt",
1518
- requestedProvider: input.provider,
1519
- requestedModel: input.model,
1520
- supportedModels: SUPPORTED_STT_MODELS,
1521
- defaultModelForProvider: (provider) => defaultSpeechModelFor("stt", provider)
1522
- });
1523
- const retryDeadlineMs = providerLeaseRetryDeadlineMs();
1524
- for (;;) {
1525
- let lastError = null;
1526
- for (const attempt of attempts) {
1527
- try {
1528
- return await this.transcribeSpeechWithProvider({
1529
- ...input,
1530
- provider: attempt.provider,
1531
- model: attempt.model
1532
- });
1533
- }
1534
- catch (error) {
1535
- lastError = error;
1536
- if (!isRecoverableProviderAttemptError(error)) {
1537
- throw error;
1538
- }
1539
- }
1540
- }
1541
- if (await waitForRetryableProviderError(lastError, retryDeadlineMs)) {
1542
- continue;
1543
- }
1544
- throw lastError instanceof Error ? lastError : new ProviderLeaseTimeoutError("No eligible AI speech-transcription provider key became available in time.");
1545
- }
1546
- }
1547
- async transcribeSpeechWithProvider(input) {
1548
- assertSupportedSpeechModel("stt", input.provider, input.model);
1549
- const lease = await this.leaseCustomerKey({
1550
- customerId: input.customerId,
1551
- provider: input.provider,
1552
- jobId: input.jobId,
1553
- workerId: input.workerId
1554
- });
1555
- try {
1556
- if (config.mockProviders) {
1557
- await this.serverlessProviderKeys.recordProviderKeyUsage({
1558
- id: createId("usage"),
1559
- keyId: lease.keyId,
1560
- jobId: input.jobId,
1561
- provider: input.provider,
1562
- model: input.model,
1563
- eventType: "success",
1564
- inputTokens: 0,
1565
- outputTokens: 0,
1566
- metadata: { mock: true, type: "speech_transcription" }
1567
- });
1568
- await this.serverlessProviderKeys.touchProviderKey(lease.keyId);
1569
- return {
1570
- text: "Mock transcription output.",
1571
- usage: {
1572
- inputTokens: 0,
1573
- outputTokens: 0,
1574
- costUsd: 0
1575
- }
1576
- };
1577
- }
1578
- const result = zeroUsageCost(await this.withLeaseRenewal(lease, () => input.provider === "gemini"
1579
- ? this.callGeminiSpeechTranscription({
1580
- model: input.model,
1581
- audio: input.audio,
1582
- contentType: input.contentType,
1583
- prompt: input.prompt,
1584
- language: input.language,
1585
- apiKey: lease.secret
1586
- })
1587
- : input.provider === "openrouter"
1588
- ? this.callOpenRouterSpeechTranscription({
1589
- model: input.model,
1590
- audio: input.audio,
1591
- contentType: input.contentType,
1592
- prompt: input.prompt,
1593
- language: input.language,
1594
- apiKey: lease.secret
1595
- })
1596
- : this.callOpenAISpeechTranscription({
1597
- model: input.model,
1598
- audio: input.audio,
1599
- contentType: input.contentType,
1600
- prompt: input.prompt,
1601
- language: input.language,
1602
- apiKey: lease.secret
1603
- })));
1604
- await this.serverlessProviderKeys.recordProviderKeyUsage({
1605
- id: createId("usage"),
1606
- keyId: lease.keyId,
1607
- jobId: input.jobId,
1608
- provider: input.provider,
1609
- model: input.model,
1610
- eventType: "success",
1611
- inputTokens: result.usage.inputTokens,
1612
- outputTokens: result.usage.outputTokens,
1613
- metadata: { type: "speech_transcription" }
1614
- });
1615
- await this.serverlessProviderKeys.touchProviderKey(lease.keyId);
1616
- return result;
1617
- }
1618
- catch (error) {
1619
- return await this.handleLeasedProviderError({
1620
- error,
1621
- lease,
1622
- jobId: input.jobId,
1623
- provider: input.provider,
1624
- model: input.model,
1625
- metadata: { type: "speech_transcription" }
1626
- });
1627
- }
1628
- finally {
1629
- await this.releaseLease(lease);
1630
- }
1631
- }
1632
- async callOpenAIImageGeneration(input) {
1633
- const prompt = input.aspectRatio === "9:16"
1634
- ? [
1635
- input.prompt,
1636
- "Return a native 9:16 vertical composition with no letterboxing, no inset landscape window, and no empty top or bottom padding."
1637
- ].join("\n")
1638
- : input.prompt;
1639
- if (input.promptAttachments?.length) {
1640
- return this.callOpenAIImageEditRequest({
1641
- model: input.model,
1642
- prompt,
1643
- imageUrls: input.promptAttachments,
1644
- size: input.size ?? "1024x1536",
1645
- apiKey: input.apiKey
1646
- });
1647
- }
1648
- const response = await fetch("https://api.openai.com/v1/images/generations", {
1649
- method: "POST",
1650
- headers: {
1651
- "Content-Type": "application/json",
1652
- Authorization: `Bearer ${input.apiKey}`
1653
- },
1654
- body: JSON.stringify({
1655
- model: input.model,
1656
- prompt,
1657
- size: input.size ?? "1024x1536"
1658
- })
1659
- });
1660
- if (response.status === 401) {
1661
- throw new ProviderAuthError("openai authentication failed");
1662
- }
1663
- if (response.status === 429) {
1664
- throw await rateLimitOrQuotaError("openai", response);
1665
- }
1666
- if (!response.ok) {
1667
- const details = await response.text();
1668
- throw new Error(`openai image generation returned ${response.status}${details ? `: ${details}` : ""}`);
1669
- }
1670
- const data = await response.json();
1671
- const encoded = data.data?.[0]?.b64_json;
1672
- if (!encoded) {
1673
- throw new Error("openai image generation returned no image payload");
1674
- }
1675
- return {
1676
- bytes: Buffer.from(encoded, "base64"),
1677
- contentType: "image/png",
1678
- revisedPrompt: data.data?.[0]?.revised_prompt ?? null
1679
- };
1680
- }
1681
- async callOpenAIImageEdit(input) {
1682
- return this.callOpenAIImageEditRequest({
1683
- model: input.model,
1684
- prompt: input.instruction,
1685
- imageUrls: [input.sourceImageUrl, ...(input.referenceAttachments ?? [])],
1686
- maskUrl: input.maskUrl,
1687
- size: input.size ?? "auto",
1688
- apiKey: input.apiKey
1689
- });
1690
- }
1691
- async callOpenAIImageEditRequest(input) {
1692
- if (input.imageUrls.length > 16) {
1693
- throw new Error("OpenAI image edits support at most 16 input images.");
1694
- }
1695
- const form = new FormData();
1696
- form.set("model", input.model);
1697
- form.set("prompt", input.prompt);
1698
- form.set("size", input.size);
1699
- const imageFieldName = input.imageUrls.length > 1 ? "image[]" : "image";
1700
- for (let index = 0; index < input.imageUrls.length; index += 1) {
1701
- const image = await remoteImageToBlob(input.imageUrls[index]);
1702
- form.append(imageFieldName, image, openAIImageFilename(index, image.type));
1703
- }
1704
- if (input.maskUrl) {
1705
- form.set("mask", await remoteImageToBlob(input.maskUrl));
1706
- }
1707
- const response = await fetch("https://api.openai.com/v1/images/edits", {
1708
- method: "POST",
1709
- headers: {
1710
- Authorization: `Bearer ${input.apiKey}`
1711
- },
1712
- body: form
1713
- });
1714
- if (response.status === 401) {
1715
- throw new ProviderAuthError("openai authentication failed");
1716
- }
1717
- if (response.status === 429) {
1718
- throw await rateLimitOrQuotaError("openai", response);
1719
- }
1720
- if (!response.ok) {
1721
- const details = await response.text();
1722
- throw new Error(`openai image edit returned ${response.status}${details ? `: ${details}` : ""}`);
1723
- }
1724
- const data = await response.json();
1725
- const encoded = data.data?.[0]?.b64_json;
1726
- if (!encoded) {
1727
- throw new Error("openai image edit returned no image payload");
1728
- }
1729
- return {
1730
- bytes: Buffer.from(encoded, "base64"),
1731
- contentType: "image/png",
1732
- revisedPrompt: data.data?.[0]?.revised_prompt ?? null
1733
- };
1734
- }
1735
- async callAttachmentDrivenImageEdit(input) {
1736
- const promptAttachments = [
1737
- input.sourceImageUrl,
1738
- ...(input.maskUrl ? [input.maskUrl] : []),
1739
- ...(input.referenceAttachments ?? [])
1740
- ];
1741
- const prompt = [
1742
- input.maskUrl
1743
- ? "Edit this image using the provided source image and mask."
1744
- : "Edit this image using the provided source image as the base reference.",
1745
- input.maskUrl
1746
- ? "Only change the masked region. Preserve all unmasked regions exactly."
1747
- : "Preserve the overall composition unless the instruction explicitly changes it.",
1748
- `Instruction: ${input.instruction}`,
1749
- "Return one final edited image only."
1750
- ].join("\n");
1751
- return input.provider === "gemini"
1752
- ? this.callGeminiImageGeneration({
1753
- model: input.model,
1754
- prompt,
1755
- promptAttachments,
1756
- aspectRatio: input.aspectRatio,
1757
- imageSize: input.imageSize,
1758
- apiKey: input.apiKey
1759
- })
1760
- : this.callOpenRouterImageGeneration({
1761
- model: input.model,
1762
- prompt,
1763
- promptAttachments,
1764
- aspectRatio: input.aspectRatio,
1765
- imageSize: input.imageSize,
1766
- apiKey: input.apiKey
1767
- });
1768
- }
1769
- async callGeminiImageGeneration(input) {
1770
- const imageConfig = {};
1771
- if (input.aspectRatio) {
1772
- imageConfig.aspectRatio = input.aspectRatio;
1773
- }
1774
- if (input.imageSize && supportsGeminiImageSize(input.model)) {
1775
- imageConfig.imageSize = input.imageSize;
1776
- }
1777
- const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${input.model}:generateContent?key=${input.apiKey}`, {
1778
- method: "POST",
1779
- headers: {
1780
- "Content-Type": "application/json"
1781
- },
1782
- body: JSON.stringify({
1783
- contents: [{ parts: await buildGeminiPromptParts(input.prompt, input.promptAttachments ?? []) }],
1784
- generationConfig: {
1785
- responseModalities: ["Image"],
1786
- ...(Object.keys(imageConfig).length
1787
- ? {
1788
- imageConfig
1789
- }
1790
- : {})
1791
- }
1792
- })
1793
- });
1794
- if (response.status === 401 || response.status === 403) {
1795
- throw new ProviderAuthError("gemini authentication failed");
1796
- }
1797
- if (response.status === 429) {
1798
- throw await rateLimitOrQuotaError("gemini", response);
1799
- }
1800
- if (!response.ok) {
1801
- const details = await response.text();
1802
- throw new Error(`gemini image generation returned ${response.status}${details ? `: ${details}` : ""}`);
1803
- }
1804
- const data = await response.json();
1805
- const part = data.candidates?.[0]?.content?.parts?.find((item) => item.inlineData?.data);
1806
- const encoded = part?.inlineData?.data;
1807
- if (!encoded) {
1808
- throw new Error("gemini image generation returned no image payload");
1809
- }
1810
- return {
1811
- bytes: Buffer.from(encoded, "base64"),
1812
- contentType: part.inlineData?.mimeType ?? "image/png",
1813
- revisedPrompt: null
1814
- };
1815
- }
1816
- async callOpenRouterImageGeneration(input) {
1817
- const requestBody = {
1818
- model: input.model,
1819
- prompt: input.prompt
1820
- };
1821
- if (input.promptAttachments?.length) {
1822
- requestBody.input_references = input.promptAttachments;
1823
- }
1824
- if (input.aspectRatio) {
1825
- requestBody.aspect_ratio = input.aspectRatio;
1826
- }
1827
- if (input.imageSize) {
1828
- requestBody.image_size = input.imageSize;
1829
- }
1830
- const response = await fetch("https://openrouter.ai/api/v1/images", {
1831
- method: "POST",
1832
- headers: {
1833
- "Content-Type": "application/json",
1834
- Authorization: `Bearer ${input.apiKey}`
1835
- },
1836
- body: JSON.stringify(requestBody)
1837
- });
1838
- if (response.status === 401 || response.status === 403) {
1839
- throw new ProviderAuthError("openrouter authentication failed");
1840
- }
1841
- if (response.status === 429) {
1842
- throw await rateLimitOrQuotaError("openrouter", response);
1843
- }
1844
- if (!response.ok) {
1845
- const details = await response.text();
1846
- throw new Error(`openrouter image generation returned ${response.status}${details ? `: ${details}` : ""}`);
1847
- }
1848
- const data = await response.json();
1849
- const image = await parseOpenRouterImageGenerationPayload(data);
1850
- if (!image) {
1851
- const details = conciseOpenRouterImageResponse(data);
1852
- throw new Error(`openrouter image generation returned no image payload${details ? `: ${details}` : ""}`);
1853
- }
1854
- return image;
1855
- }
1856
- async callOpenAICompatibleSpeechGeneration(input) {
1857
- const requestedFormat = input.responseFormat ?? "mp3";
1858
- const usePcmBridge = input.provider === "openrouter" &&
1859
- /gemini/i.test(input.model) &&
1860
- requestedFormat !== "mp3";
1861
- const responseFormat = usePcmBridge ? "pcm" : requestedFormat;
1862
- const endpoint = input.provider === "openrouter"
1863
- ? "https://openrouter.ai/api/v1/audio/speech"
1864
- : "https://api.openai.com/v1/audio/speech";
1865
- const response = await fetch(endpoint, {
1866
- method: "POST",
1867
- headers: {
1868
- "Content-Type": "application/json",
1869
- Authorization: `Bearer ${input.apiKey}`
1870
- },
1871
- body: JSON.stringify({
1872
- model: input.model,
1873
- input: input.text,
1874
- voice: input.voice ?? "alloy",
1875
- instructions: input.instructions,
1876
- response_format: responseFormat
1877
- })
1878
- });
1879
- if (response.status === 401 || response.status === 403) {
1880
- throw new ProviderAuthError(`${input.provider} authentication failed`);
1881
- }
1882
- if (response.status === 429) {
1883
- throw await rateLimitOrQuotaError(input.provider, response);
1884
- }
1885
- if (!response.ok) {
1886
- const details = await response.text();
1887
- throw new Error(`${input.provider} speech generation returned ${response.status}${details ? `: ${details}` : ""}`);
1888
- }
1889
- const rawBytes = Buffer.from(await response.arrayBuffer());
1890
- const bytes = usePcmBridge ? pcm16MonoToWav(rawBytes, 24000) : rawBytes;
1891
- const contentType = usePcmBridge
1892
- ? "audio/wav"
1893
- : response.headers.get("content-type")?.split(";")[0]?.trim() || inferSpeechContentType(responseFormat);
1894
- return {
1895
- bytes,
1896
- contentType,
1897
- usage: {
1898
- inputTokens: 0,
1899
- outputTokens: 0,
1900
- costUsd: 0
1901
- }
1902
- };
1903
- }
1904
- async callGeminiSpeechGeneration(input) {
1905
- const prompt = input.instructions?.trim()
1906
- ? `${input.instructions.trim()}\n\nSpeak exactly the following text:\n${input.text}`
1907
- : input.text;
1908
- const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${input.model}:generateContent?key=${input.apiKey}`, {
1909
- method: "POST",
1910
- headers: {
1911
- "Content-Type": "application/json"
1912
- },
1913
- body: JSON.stringify({
1914
- contents: [{ parts: [{ text: prompt }] }],
1915
- generationConfig: {
1916
- responseModalities: ["AUDIO"],
1917
- speechConfig: {
1918
- voiceConfig: {
1919
- prebuiltVoiceConfig: {
1920
- voiceName: input.voice ?? "Kore"
1921
- }
1922
- }
1923
- }
1924
- }
1925
- })
1926
- });
1927
- if (response.status === 401 || response.status === 403) {
1928
- throw new ProviderAuthError("gemini authentication failed");
1929
- }
1930
- if (response.status === 429) {
1931
- throw await rateLimitOrQuotaError("gemini", response);
1932
- }
1933
- if (!response.ok) {
1934
- const details = await response.text();
1935
- throw new Error(`gemini speech generation returned ${response.status}${details ? `: ${details}` : ""}`);
1936
- }
1937
- const data = await response.json();
1938
- const part = data.candidates?.[0]?.content?.parts?.find((item) => item.inlineData?.data || item.inline_data?.data);
1939
- const encoded = part?.inlineData?.data ?? part?.inline_data?.data;
1940
- if (!encoded) {
1941
- throw new Error("gemini speech generation returned no audio payload");
1942
- }
1943
- const rawBytes = Buffer.from(encoded, "base64");
1944
- const rawContentType = String(part?.inlineData?.mimeType ?? part?.inline_data?.mime_type ?? "audio/L16;rate=24000");
1945
- const pcmRateMatch = /audio\/l16;?\s*rate=(\d+)/i.exec(rawContentType);
1946
- const looksLikeWav = isWavBuffer(rawBytes);
1947
- const looksLikeMp3 = isLikelyMp3Buffer(rawBytes);
1948
- const shouldWrapPcm = Boolean(pcmRateMatch ||
1949
- (!looksLikeWav && !looksLikeMp3));
1950
- const bytes = shouldWrapPcm
1951
- ? pcm16MonoToWav(rawBytes, Number(pcmRateMatch?.[1] || "24000"))
1952
- : rawBytes;
1953
- const contentType = shouldWrapPcm
1954
- ? "audio/wav"
1955
- : looksLikeWav
1956
- ? "audio/wav"
1957
- : "audio/mpeg";
1958
- return {
1959
- bytes,
1960
- contentType,
1961
- usage: {
1962
- inputTokens: Number(data.usageMetadata?.promptTokenCount ?? 0),
1963
- outputTokens: Number(data.usageMetadata?.candidatesTokenCount ?? 0),
1964
- costUsd: 0
1965
- }
1966
- };
1967
- }
1968
- async callOpenAICompatibleLayoutAnalysis(input) {
1969
- const endpoint = input.provider === "openrouter"
1970
- ? "https://openrouter.ai/api/v1/chat/completions"
1971
- : "https://api.openai.com/v1/chat/completions";
1972
- const response = await fetch(endpoint, {
1973
- method: "POST",
1974
- headers: {
1975
- "Content-Type": "application/json",
1976
- Authorization: `Bearer ${input.apiKey}`
1977
- },
1978
- body: JSON.stringify({
1979
- model: input.model,
1980
- temperature: 0.1,
1981
- response_format: { type: "json_object" },
1982
- messages: [
1983
- {
1984
- role: "system",
1985
- content: "Analyze a 9:16 slideshow image and return JSON with zone, align, maxWidthPercent, justification. Prefer centered text placement in the middle third of the frame unless that would cover the subject. Avoid covering the subject, avoid fake caption panels, and keep the caption inside a TikTok-safe region that avoids the top tabs, right-side action rail, and bottom caption/audio UI."
1986
- },
1987
- {
1988
- role: "user",
1989
- content: [
1990
- {
1991
- type: "text",
1992
- text: `The exact overlay text is: ${input.overlayText}\nChoose zone from top, center, bottom. Choose align from left, center, right. Prefer center when viable. maxWidthPercent must be between 46 and 62.\nTreat the top 12 percent, right 22 percent, and bottom 20 percent as reserved TikTok UI chrome.\nThe visual goal is native TikTok caption placement, not poster typography.`
1993
- },
1994
- {
1995
- type: "image_url",
1996
- image_url: {
1997
- url: input.imageUrl
1998
- }
1999
- }
2000
- ]
2001
- }
2002
- ]
2003
- })
2004
- });
2005
- if (response.status === 401) {
2006
- throw new ProviderAuthError(`${input.provider} authentication failed`);
2007
- }
2008
- if (response.status === 429) {
2009
- throw await rateLimitOrQuotaError(input.provider, response);
2010
- }
2011
- if (!response.ok) {
2012
- throw new Error(`${input.provider} layout analysis returned ${response.status}`);
2013
- }
2014
- const data = await response.json();
2015
- return String(data.choices?.[0]?.message?.content ?? "");
2016
- }
2017
- async callGeminiLayoutAnalysis(input) {
2018
- const image = await fetch(input.imageUrl);
2019
- if (!image.ok) {
2020
- throw new Error(`could not fetch image for gemini layout analysis: ${image.status}`);
2021
- }
2022
- const bytes = Buffer.from(await image.arrayBuffer());
2023
- const mimeType = image.headers.get("content-type") || "image/png";
2024
- const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${input.model}:generateContent?key=${input.apiKey}`, {
2025
- method: "POST",
2026
- headers: {
2027
- "Content-Type": "application/json"
2028
- },
2029
- body: JSON.stringify({
2030
- contents: [{
2031
- parts: [
2032
- {
2033
- text: [
2034
- "Analyze this 9:16 slideshow image and return JSON with keys zone, align, maxWidthPercent, justification.",
2035
- "zone must be one of top, center, bottom.",
2036
- "align must be one of left, center, right.",
2037
- "Prefer centered text placement in the middle third unless that would cover the subject.",
2038
- "maxWidthPercent must be between 46 and 62.",
2039
- "Prefer negative space and avoid covering the subject.",
2040
- "Treat the top 12 percent, right 22 percent, and bottom 20 percent as reserved TikTok UI chrome.",
2041
- "The visual goal is native TikTok caption placement, not poster typography or large title cards.",
2042
- `Exact overlay text: ${input.overlayText}`
2043
- ].join("\n")
2044
- },
2045
- {
2046
- inline_data: {
2047
- mime_type: mimeType,
2048
- data: bytes.toString("base64")
2049
- }
2050
- }
2051
- ]
2052
- }],
2053
- generationConfig: {
2054
- responseMimeType: "application/json"
2055
- }
2056
- })
2057
- });
2058
- if (response.status === 401 || response.status === 403) {
2059
- throw new ProviderAuthError("gemini authentication failed");
2060
- }
2061
- if (response.status === 429) {
2062
- throw await rateLimitOrQuotaError("gemini", response);
2063
- }
2064
- if (!response.ok) {
2065
- throw new Error(`gemini layout analysis returned ${response.status}`);
2066
- }
2067
- const data = await response.json();
2068
- const text = data.candidates?.[0]?.content?.parts?.map((part) => part.text ?? "").join("\n") ?? "";
2069
- return String(text);
2070
- }
2071
- async callOpenAISpeechTranscription(input) {
2072
- const contentType = input.contentType ?? "audio/mpeg";
2073
- const form = new FormData();
2074
- form.set("file", new Blob([Buffer.from(input.audio)], { type: contentType }), `audio.${audioExtensionFromContentType(contentType)}`);
2075
- form.set("model", input.model);
2076
- form.set("response_format", "json");
2077
- if (input.prompt) {
2078
- form.set("prompt", input.prompt);
2079
- }
2080
- if (input.language) {
2081
- form.set("language", input.language);
2082
- }
2083
- const response = await fetch("https://api.openai.com/v1/audio/transcriptions", {
2084
- method: "POST",
2085
- headers: {
2086
- Authorization: `Bearer ${input.apiKey}`
2087
- },
2088
- body: form
2089
- });
2090
- if (response.status === 401 || response.status === 403) {
2091
- throw new ProviderAuthError("openai authentication failed");
2092
- }
2093
- if (response.status === 429) {
2094
- throw await rateLimitOrQuotaError("openai", response);
2095
- }
2096
- if (!response.ok) {
2097
- const details = await response.text();
2098
- throw new Error(`openai transcription returned ${response.status}${details ? `: ${details}` : ""}`);
2099
- }
2100
- const data = await response.json();
2101
- return {
2102
- text: String(data.text ?? ""),
2103
- usage: {
2104
- inputTokens: Number(data.usage?.input_tokens ?? 0),
2105
- outputTokens: Number(data.usage?.output_tokens ?? 0),
2106
- costUsd: Number(data.usage?.cost ?? 0)
2107
- }
2108
- };
2109
- }
2110
- async callOpenRouterSpeechTranscription(input) {
2111
- const response = await fetch("https://openrouter.ai/api/v1/audio/transcriptions", {
2112
- method: "POST",
2113
- headers: {
2114
- "Content-Type": "application/json",
2115
- Authorization: `Bearer ${input.apiKey}`
2116
- },
2117
- body: JSON.stringify({
2118
- model: input.model,
2119
- input_audio: {
2120
- data: Buffer.from(input.audio).toString("base64"),
2121
- format: audioFormatFromContentType(input.contentType)
2122
- },
2123
- ...(input.prompt ? { prompt: input.prompt } : {}),
2124
- ...(input.language ? { language: input.language } : {})
2125
- })
2126
- });
2127
- if (response.status === 401 || response.status === 403) {
2128
- throw new ProviderAuthError("openrouter authentication failed");
2129
- }
2130
- if (response.status === 429) {
2131
- throw await rateLimitOrQuotaError("openrouter", response);
2132
- }
2133
- if (!response.ok) {
2134
- const details = await response.text();
2135
- throw new Error(`openrouter transcription returned ${response.status}${details ? `: ${details}` : ""}`);
2136
- }
2137
- const data = await response.json();
2138
- return {
2139
- text: String(data.text ?? ""),
2140
- usage: {
2141
- inputTokens: Number(data.usage?.input_tokens ?? data.usage?.prompt_tokens ?? 0),
2142
- outputTokens: Number(data.usage?.output_tokens ?? data.usage?.completion_tokens ?? 0),
2143
- costUsd: Number(data.usage?.cost ?? 0)
2144
- }
2145
- };
2146
- }
2147
- async callGeminiSpeechTranscription(input) {
2148
- const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${input.model}:generateContent?key=${input.apiKey}`, {
2149
- method: "POST",
2150
- headers: {
2151
- "Content-Type": "application/json"
2152
- },
2153
- body: JSON.stringify({
2154
- contents: [{
2155
- parts: [
2156
- {
2157
- inline_data: {
2158
- mime_type: input.contentType ?? "audio/mpeg",
2159
- data: Buffer.from(input.audio).toString("base64")
2160
- }
2161
- },
2162
- {
2163
- text: buildGeminiTranscriptionPrompt(input.prompt, input.language)
2164
- }
2165
- ]
2166
- }],
2167
- generationConfig: {
2168
- temperature: 0,
2169
- responseMimeType: "text/plain"
2170
- }
2171
- })
2172
- });
2173
- if (response.status === 401 || response.status === 403) {
2174
- throw new ProviderAuthError("gemini authentication failed");
2175
- }
2176
- if (response.status === 429) {
2177
- throw await rateLimitOrQuotaError("gemini", response);
2178
- }
2179
- if (!response.ok) {
2180
- const details = await response.text();
2181
- throw new Error(`gemini transcription returned ${response.status}${details ? `: ${details}` : ""}`);
2182
- }
2183
- const data = await response.json();
2184
- return {
2185
- text: extractGeminiText(data),
2186
- usage: {
2187
- inputTokens: Number(data.usageMetadata?.promptTokenCount ?? 0),
2188
- outputTokens: Number(data.usageMetadata?.candidatesTokenCount ?? 0),
2189
- costUsd: 0
2190
- }
2191
- };
2192
- }
2193
- async callProvider(input) {
2194
- if (input.provider === "gemini") {
2195
- return this.callGemini({
2196
- provider: "gemini",
2197
- model: input.model,
2198
- prompt: input.prompt,
2199
- temperature: input.temperature,
2200
- responseFormat: input.responseFormat,
2201
- apiKey: input.apiKey
2202
- });
2203
- }
2204
- return this.callOpenAICompatible(input);
2205
- }
2206
- async callOpenAICompatible(input) {
2207
- const endpoint = input.provider === "openrouter"
2208
- ? "https://openrouter.ai/api/v1/chat/completions"
2209
- : input.provider === "perplexity"
2210
- ? "https://api.perplexity.ai/chat/completions"
2211
- : "https://api.openai.com/v1/chat/completions";
2212
- const response = await fetch(endpoint, {
2213
- method: "POST",
2214
- headers: {
2215
- "Content-Type": "application/json",
2216
- Authorization: `Bearer ${input.apiKey}`
2217
- },
2218
- body: JSON.stringify({
2219
- model: input.model,
2220
- temperature: input.temperature ?? 0.7,
2221
- ...(input.responseFormat === "json" ? { response_format: { type: "json_object" } } : {}),
2222
- messages: [{ role: "user", content: input.prompt }]
2223
- })
2224
- });
2225
- if (response.status === 401) {
2226
- throw new ProviderAuthError(`${input.provider} authentication failed`);
2227
- }
2228
- if (response.status === 429) {
2229
- throw await rateLimitOrQuotaError(input.provider, response);
2230
- }
2231
- if (!response.ok) {
2232
- throw new Error(`${input.provider} returned ${response.status}`);
2233
- }
2234
- const data = await response.json();
2235
- return {
2236
- text: data.choices?.[0]?.message?.content ?? "",
2237
- usage: {
2238
- inputTokens: Number(data.usage?.prompt_tokens ?? 0),
2239
- outputTokens: Number(data.usage?.completion_tokens ?? 0),
2240
- costUsd: 0
2241
- }
2242
- };
2243
- }
2244
- async callGemini(input) {
2245
- const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${input.model}:generateContent?key=${input.apiKey}`, {
2246
- method: "POST",
2247
- headers: {
2248
- "Content-Type": "application/json"
2249
- },
2250
- body: JSON.stringify({
2251
- contents: [{ parts: [{ text: input.prompt }] }],
2252
- generationConfig: {
2253
- temperature: input.temperature ?? 0.7,
2254
- ...(input.responseFormat === "json" ? { responseMimeType: "application/json" } : {})
2255
- }
2256
- })
2257
- });
2258
- if (response.status === 401 || response.status === 403) {
2259
- throw new ProviderAuthError("Gemini authentication failed");
2260
- }
2261
- if (response.status === 429) {
2262
- throw await rateLimitOrQuotaError("gemini", response);
2263
- }
2264
- if (!response.ok) {
2265
- throw new Error(`Gemini returned ${response.status}`);
2266
- }
2267
- const data = await response.json();
2268
- return {
2269
- text: data.candidates?.[0]?.content?.parts?.map((part) => part.text ?? "").join("\n") ?? "",
2270
- usage: {
2271
- inputTokens: Number(data.usageMetadata?.promptTokenCount ?? 0),
2272
- outputTokens: Number(data.usageMetadata?.candidatesTokenCount ?? 0),
2273
- costUsd: 0
2274
- }
2275
- };
2276
- }
2277
- }
2278
- function clamp(value, min, max) {
2279
- return Math.max(min, Math.min(max, value));
2280
- }
2281
- function summarizeProviderValidationError(error) {
2282
- const rawMessage = error instanceof Error ? error.message : "Unknown provider error";
2283
- return rawMessage
2284
- .replace(/\s+/g, " ")
2285
- .trim()
2286
- .slice(0, 280);
2287
- }
2288
- function toProviderValidationMessage(provider, error) {
2289
- const providerLabel = formatProviderLabel(provider);
2290
- const summary = summarizeProviderValidationError(error);
2291
- if (!summary) {
2292
- return `Unable to verify ${providerLabel} API key.`;
2293
- }
2294
- if (/401|403|invalid api key|api key not valid|incorrect api key|unauthorized|permission denied|authentication failed/i.test(summary)) {
2295
- return `${providerLabel} API key was rejected by the provider. ${summary}`;
2296
- }
2297
- return `Unable to verify ${providerLabel} API key. ${summary}`;
2298
- }
2299
- function formatProviderLabel(provider) {
2300
- switch (provider) {
2301
- case "openai":
2302
- return "OpenAI";
2303
- case "gemini":
2304
- return "Gemini";
2305
- case "openrouter":
2306
- return "OpenRouter";
2307
- case "perplexity":
2308
- return "Perplexity";
2309
- }
2310
- }
2311
- function readStringField(value, field) {
2312
- const raw = value[field];
2313
- return typeof raw === "string" && raw.trim() ? raw.trim() : null;
2314
- }
2315
- function normalizeOpenRouterUrl(value) {
2316
- if (!value) {
2317
- return null;
2318
- }
2319
- if (/^https?:\/\//i.test(value)) {
2320
- return value;
2321
- }
2322
- if (value.startsWith("/")) {
2323
- return `https://openrouter.ai${value}`;
2324
- }
2325
- return `https://openrouter.ai/${value}`;
2326
- }
2327
- function openRouterVideoStatusUrl(jobId) {
2328
- return jobId ? `https://openrouter.ai/api/v1/videos/${encodeURIComponent(jobId)}` : null;
2329
- }
2330
- function findOpenRouterVideoUrl(value) {
2331
- if (!value || typeof value !== "object") {
2332
- return null;
2333
- }
2334
- if (Array.isArray(value)) {
2335
- for (const item of value) {
2336
- const found = typeof item === "string" ? item : findOpenRouterVideoUrl(item);
2337
- if (found && isOpenRouterVideoContentUrl(found)) {
2338
- return normalizeOpenRouterUrl(found);
2339
- }
2340
- }
2341
- return null;
2342
- }
2343
- const record = value;
2344
- const unsignedUrls = record.unsigned_urls;
2345
- if (Array.isArray(unsignedUrls)) {
2346
- const found = unsignedUrls.find((item) => typeof item === "string" && item.trim().length > 0);
2347
- if (found) {
2348
- return normalizeOpenRouterUrl(found);
2349
- }
2350
- }
2351
- for (const key of ["video_url", "output_url", "download_url", "file_url", "content_url"]) {
2352
- const raw = record[key];
2353
- if (typeof raw === "string" && raw.trim()) {
2354
- return normalizeOpenRouterUrl(raw.trim());
2355
- }
2356
- }
2357
- return null;
2358
- }
2359
- function isOpenRouterVideoContentUrl(value) {
2360
- return /^https?:\/\//i.test(value) || value.startsWith("/");
2361
- }
2362
- function readOpenRouterUsageCost(value) {
2363
- const usage = value.usage;
2364
- if (usage && typeof usage === "object") {
2365
- const cost = Number(usage.cost ?? usage.total_cost);
2366
- if (Number.isFinite(cost) && cost > 0) {
2367
- return cost;
2368
- }
2369
- }
2370
- const cost = Number(value.cost ?? value.total_cost);
2371
- return Number.isFinite(cost) && cost > 0 ? cost : 0;
2372
- }
2373
- function readOpenAIUsageCost(value) {
2374
- const usage = value.usage;
2375
- if (usage && typeof usage === "object") {
2376
- const cost = Number(usage.cost ?? usage.total_cost);
2377
- if (Number.isFinite(cost) && cost > 0) {
2378
- return cost;
2379
- }
2380
- }
2381
- return 0;
2382
- }
2383
- function normalizeOpenAIVideoSize(model, size, aspectRatio, resolution) {
2384
- const requested = size ?? openAIVideoSizeForAspect(aspectRatio, resolution);
2385
- if (!requested) {
2386
- return undefined;
2387
- }
2388
- const parsed = parsePixelSize(requested);
2389
- const requestedAspect = parsed
2390
- ? (parsed.width >= parsed.height ? "16:9" : "9:16")
2391
- : aspectRatio;
2392
- const isPortrait = requestedAspect !== "16:9";
2393
- if (model === "sora-2-pro") {
2394
- if (requested === "1024x1792" || requested === "1792x1024" || requested === "720x1280" || requested === "1280x720") {
2395
- return requested;
2396
- }
2397
- return resolution === "1080p"
2398
- ? isPortrait ? "1024x1792" : "1792x1024"
2399
- : isPortrait ? "720x1280" : "1280x720";
2400
- }
2401
- if (model === "sora-2") {
2402
- return isPortrait ? "720x1280" : "1280x720";
2403
- }
2404
- return requested;
2405
- }
2406
- function openAIVideoSizeForAspect(aspectRatio, resolution) {
2407
- if (resolution === "1080p") {
2408
- if (aspectRatio === "9:16")
2409
- return "1024x1792";
2410
- if (aspectRatio === "16:9")
2411
- return "1792x1024";
2412
- }
2413
- if (aspectRatio === "9:16") {
2414
- return "720x1280";
2415
- }
2416
- if (aspectRatio === "16:9") {
2417
- return "1280x720";
2418
- }
2419
- return undefined;
2420
- }
2421
- function openAIVideoSeconds(duration) {
2422
- if (!duration) {
2423
- return undefined;
2424
- }
2425
- if (duration >= 10) {
2426
- return "12";
2427
- }
2428
- if (duration >= 6) {
2429
- return "8";
2430
- }
2431
- return "4";
2432
- }
2433
- function geminiVideoAspectRatio(aspectRatio) {
2434
- return aspectRatio === "9:16" ? "9:16" : aspectRatio === "16:9" ? "16:9" : undefined;
2435
- }
2436
- function geminiVideoResolution(resolution) {
2437
- return resolution === "1080p" ? "1080p" : resolution === "720p" ? "720p" : undefined;
2438
- }
2439
- function geminiVideoDurationSeconds(duration) {
2440
- if (!duration) {
2441
- return undefined;
2442
- }
2443
- if (duration >= 7) {
2444
- return 8;
2445
- }
2446
- if (duration >= 5) {
2447
- return 6;
2448
- }
2449
- return 4;
2450
- }
2451
- function findGeminiVideoUri(value) {
2452
- if (!value || typeof value !== "object") {
2453
- return null;
2454
- }
2455
- if (Array.isArray(value)) {
2456
- for (const item of value) {
2457
- const found = findGeminiVideoUri(item);
2458
- if (found) {
2459
- return found;
2460
- }
2461
- }
2462
- return null;
2463
- }
2464
- const record = value;
2465
- for (const key of ["uri", "videoUri", "fileUri", "downloadUri"]) {
2466
- const raw = record[key];
2467
- if (typeof raw === "string" && /^https?:\/\//i.test(raw)) {
2468
- return raw;
2469
- }
2470
- }
2471
- for (const nested of Object.values(record)) {
2472
- const found = findGeminiVideoUri(nested);
2473
- if (found) {
2474
- return found;
2475
- }
2476
- }
2477
- return null;
2478
- }
2479
- function findGeminiInlineVideo(value) {
2480
- if (!value || typeof value !== "object") {
2481
- return null;
2482
- }
2483
- if (Array.isArray(value)) {
2484
- for (const item of value) {
2485
- const found = findGeminiInlineVideo(item);
2486
- if (found) {
2487
- return found;
2488
- }
2489
- }
2490
- return null;
2491
- }
2492
- const record = value;
2493
- const encoded = typeof record.videoBytes === "string" ? record.videoBytes :
2494
- typeof record.bytesBase64Encoded === "string" ? record.bytesBase64Encoded :
2495
- typeof record.data === "string" ? record.data :
2496
- null;
2497
- if (encoded) {
2498
- return {
2499
- bytes: Buffer.from(encoded, "base64"),
2500
- contentType: typeof record.mimeType === "string" ? record.mimeType : "video/mp4"
2501
- };
2502
- }
2503
- for (const nested of Object.values(record)) {
2504
- const found = findGeminiInlineVideo(nested);
2505
- if (found) {
2506
- return found;
2507
- }
2508
- }
2509
- return null;
2510
- }
2511
- async function downloadAuthenticatedVideo(input) {
2512
- const headers = input.provider === "openai"
2513
- ? { Authorization: `Bearer ${input.apiKey}` }
2514
- : { "x-goog-api-key": input.apiKey };
2515
- const response = await fetch(input.url, { headers });
2516
- if (response.status === 401 || response.status === 403) {
2517
- throw new ProviderAuthError(`${input.provider} authentication failed`);
2518
- }
2519
- if (response.status === 402 || response.status === 429) {
2520
- throw await rateLimitOrQuotaError(input.provider, response);
2521
- }
2522
- if (!response.ok) {
2523
- const details = await response.text();
2524
- throw new Error(`${input.provider} video download returned ${response.status}${details ? `: ${details}` : ""}`);
2525
- }
2526
- return {
2527
- bytes: Buffer.from(await response.arrayBuffer()),
2528
- contentType: response.headers.get("content-type")?.split(";")[0]?.trim() || inferVideoContentType(input.url)
2529
- };
2530
- }
2531
- async function downloadGeminiVideo(url, apiKey) {
2532
- return downloadAuthenticatedVideo({
2533
- url,
2534
- apiKey,
2535
- provider: "gemini"
2536
- });
2537
- }
2538
- async function downloadOpenRouterVideo(url, apiKey) {
2539
- const includeAuth = /^https:\/\/openrouter\.ai\//i.test(url);
2540
- const response = await fetch(url, includeAuth
2541
- ? {
2542
- headers: {
2543
- Authorization: `Bearer ${apiKey}`
2544
- }
2545
- }
2546
- : {});
2547
- if (response.status === 401 || response.status === 403) {
2548
- throw new ProviderAuthError("openrouter authentication failed");
2549
- }
2550
- if (response.status === 402 || response.status === 429) {
2551
- throw await rateLimitOrQuotaError("openrouter", response);
2552
- }
2553
- if (!response.ok) {
2554
- const details = await response.text();
2555
- throw new Error(`openrouter video download returned ${response.status}${details ? `: ${details}` : ""}`);
2556
- }
2557
- const contentType = response.headers.get("content-type")?.split(";")[0]?.trim() || inferVideoContentType(url);
2558
- return {
2559
- bytes: Buffer.from(await response.arrayBuffer()),
2560
- contentType
2561
- };
2562
- }
2563
- function inferVideoContentType(url) {
2564
- if (/\.webm(\?|#|$)/i.test(url)) {
2565
- return "video/webm";
2566
- }
2567
- if (/\.mov(\?|#|$)/i.test(url)) {
2568
- return "video/quicktime";
2569
- }
2570
- return "video/mp4";
2571
- }
2572
- function assertSupportedModel(kind, provider, model, supportedModels) {
2573
- const normalizedModel = normalizeProviderModel(model);
2574
- if (!normalizedModel) {
2575
- throw new Error(`Missing ${kind} model for provider ${provider}.`);
2576
- }
2577
- const allowedModels = supportedModels[provider];
2578
- if (!allowedModels?.has(normalizedModel)) {
2579
- const allowed = allowedModels ? [...allowedModels].join(", ") : "none";
2580
- throw new Error(`Unsupported ${kind} model "${normalizedModel}" for provider ${provider}. ` +
2581
- `Templates must use Vidfarm-approved models only. Allowed models: ${allowed}.`);
2582
- }
2583
- }
2584
- function assertSupportedSpeechModel(kind, provider, model) {
2585
- assertSupportedModel(kind, provider, model, kind === "tts" ? SUPPORTED_TTS_MODELS : SUPPORTED_STT_MODELS);
2586
- }
2587
- function normalizeProviderModel(model) {
2588
- return model.trim().replace(/^models\//, "");
2589
- }
2590
- function inferSpeechContentType(format) {
2591
- return format === "wav" ? "audio/wav" : "audio/mpeg";
2592
- }
2593
- function pcm16MonoToWav(pcmBuffer, sampleRate) {
2594
- const header = Buffer.alloc(44);
2595
- header.write("RIFF", 0, 4, "ascii");
2596
- header.writeUInt32LE(36 + pcmBuffer.byteLength, 4);
2597
- header.write("WAVE", 8, 4, "ascii");
2598
- header.write("fmt ", 12, 4, "ascii");
2599
- header.writeUInt32LE(16, 16);
2600
- header.writeUInt16LE(1, 20);
2601
- header.writeUInt16LE(1, 22);
2602
- header.writeUInt32LE(sampleRate, 24);
2603
- header.writeUInt32LE(sampleRate * 2, 28);
2604
- header.writeUInt16LE(2, 32);
2605
- header.writeUInt16LE(16, 34);
2606
- header.write("data", 36, 4, "ascii");
2607
- header.writeUInt32LE(pcmBuffer.byteLength, 40);
2608
- return Buffer.concat([header, pcmBuffer]);
2609
- }
2610
- function isWavBuffer(buffer) {
2611
- return buffer.byteLength >= 12
2612
- && buffer.subarray(0, 4).toString("ascii") === "RIFF"
2613
- && buffer.subarray(8, 12).toString("ascii") === "WAVE";
2614
- }
2615
- function isLikelyMp3Buffer(buffer) {
2616
- if (buffer.byteLength < 3) {
2617
- return false;
2618
- }
2619
- if (buffer.subarray(0, 3).toString("ascii") === "ID3") {
2620
- return true;
2621
- }
2622
- return buffer[0] === 0xff && (buffer[1] & 0xe0) === 0xe0;
2623
- }
2624
- function audioFormatFromContentType(contentType) {
2625
- const normalized = contentType?.split(";")[0]?.trim().toLowerCase();
2626
- switch (normalized) {
2627
- case "audio/wav":
2628
- case "audio/x-wav":
2629
- return "wav";
2630
- case "audio/mp4":
2631
- case "audio/m4a":
2632
- return "mp4";
2633
- case "audio/webm":
2634
- return "webm";
2635
- case "audio/ogg":
2636
- return "ogg";
2637
- default:
2638
- return "mp3";
2639
- }
2640
- }
2641
- function audioExtensionFromContentType(contentType) {
2642
- switch (audioFormatFromContentType(contentType)) {
2643
- case "wav":
2644
- return "wav";
2645
- case "mp4":
2646
- return "m4a";
2647
- case "webm":
2648
- return "webm";
2649
- case "ogg":
2650
- return "ogg";
2651
- default:
2652
- return "mp3";
2653
- }
2654
- }
2655
- function buildGeminiTranscriptionPrompt(prompt, language) {
2656
- const basePrompt = prompt?.trim() || "Transcribe this audio verbatim. Return plain text only.";
2657
- return language?.trim()
2658
- ? `${basePrompt}\n\nExpected language: ${language.trim()}.`
2659
- : basePrompt;
2660
- }
2661
- function extractGeminiText(data) {
2662
- return String(data.candidates?.[0]?.content?.parts?.map((part) => part.text ?? "").join("\n") ?? "");
2663
- }
2664
- function supportsGeminiImageSize(model) {
2665
- return model === "gemini-3.1-flash-image-preview" || model === "gemini-3-pro-image-preview";
2666
- }
2667
- async function fetchRemoteBytes(input) {
2668
- const maxAttempts = isVidfarmTemplateMediaUrl(input.url) ? 6 : 4;
2669
- let lastFailure = null;
2670
- for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
2671
- let response;
2672
- try {
2673
- response = await fetch(input.url);
2674
- }
2675
- catch (error) {
2676
- lastFailure = error instanceof Error ? error.message : String(error);
2677
- if (attempt < maxAttempts - 1) {
2678
- await sleep(remoteMediaRetryDelayMs(null, attempt));
2679
- continue;
2680
- }
2681
- throw new ProviderTransientError(`Could not fetch remote media ${input.url} after ${maxAttempts} attempts: ${lastFailure}`, 5);
2682
- }
2683
- if (response.ok) {
2684
- const mimeType = response.headers.get("content-type")?.split(";")[0]?.trim() || input.fallbackMimeType || "application/octet-stream";
2685
- const bytes = Buffer.from(await response.arrayBuffer());
2686
- return { bytes, mimeType };
2687
- }
2688
- lastFailure = String(response.status);
2689
- if (!isRetryableRemoteMediaStatus(response.status, input.url)) {
2690
- throw new Error(`Could not fetch remote media ${input.url}: ${response.status}`);
2691
- }
2692
- if (attempt < maxAttempts - 1) {
2693
- await sleep(remoteMediaRetryDelayMs(response, attempt));
2694
- continue;
2695
- }
2696
- }
2697
- throw new ProviderTransientError(`Could not fetch remote media ${input.url} after ${maxAttempts} attempts: ${lastFailure ?? "unknown error"}`, 5);
2698
- }
2699
- function isRetryableRemoteMediaStatus(status, url) {
2700
- return isTransientRemoteMediaStatus(status)
2701
- || (isVidfarmTemplateMediaUrl(url) && (status === 403 || status === 404));
2702
- }
2703
- function isVidfarmTemplateMediaUrl(value) {
2704
- try {
2705
- const url = new URL(value);
2706
- return url.pathname === "/template-media" && Boolean(url.searchParams.get("key"));
2707
- }
2708
- catch {
2709
- return false;
2710
- }
2711
- }
2712
- async function remoteImageToBlob(url) {
2713
- const { bytes, mimeType } = await fetchRemoteBytes({ url, fallbackMimeType: "image/png" });
2714
- return new Blob([bytes], { type: mimeType });
2715
- }
2716
- async function remoteImageToOpenAIVideoBlob(url, size) {
2717
- const { bytes, mimeType } = await fetchRemoteBytes({ url, fallbackMimeType: "image/png" });
2718
- if (!mimeType.toLowerCase().startsWith("image/")) {
2719
- throw new Error(`OpenAI video input references must be direct image URLs. Received ${mimeType || "unknown content"} from ${url}.`);
2720
- }
2721
- const parsedSize = parsePixelSize(size);
2722
- if (!parsedSize) {
2723
- return new Blob([bytes], { type: "image/png" });
2724
- }
2725
- const normalized = await sharp(bytes)
2726
- .resize(parsedSize.width, parsedSize.height, {
2727
- fit: "cover",
2728
- position: "center"
2729
- })
2730
- .png()
2731
- .toBuffer();
2732
- return new Blob([new Uint8Array(normalized)], { type: "image/png" });
2733
- }
2734
- function parsePixelSize(size) {
2735
- const match = /^(\d+)x(\d+)$/i.exec(size ?? "");
2736
- if (!match) {
2737
- return null;
2738
- }
2739
- const width = Number(match[1]);
2740
- const height = Number(match[2]);
2741
- return Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0
2742
- ? { width, height }
2743
- : null;
2744
- }
2745
- function openAIImageFilename(index, contentType) {
2746
- const extension = contentType === "image/jpeg" ? "jpg" : contentType === "image/webp" ? "webp" : "png";
2747
- return `reference-${index + 1}.${extension}`;
2748
- }
2749
- async function buildGeminiPromptParts(prompt, attachments) {
2750
- const parts = [];
2751
- for (const attachment of attachments) {
2752
- if (!/^https?:\/\//i.test(attachment)) {
2753
- throw new Error(`Gemini prompt attachments must be public URLs: ${attachment}`);
2754
- }
2755
- const { bytes, mimeType } = await fetchRemoteBytes({ url: attachment });
2756
- parts.push({
2757
- inlineData: {
2758
- mimeType,
2759
- data: bytes.toString("base64")
2760
- }
2761
- });
2762
- }
2763
- parts.push({ text: prompt });
2764
- return parts;
2765
- }
2766
- function decodeInlineDataUrl(value) {
2767
- const match = value.match(/^data:([^;,]+);base64,(.+)$/);
2768
- if (!match) {
2769
- return null;
2770
- }
2771
- return {
2772
- contentType: match[1],
2773
- bytes: Buffer.from(match[2], "base64")
2774
- };
2775
- }
2776
- async function parseOpenRouterImageGenerationPayload(data) {
2777
- const imageData = Array.isArray(data.data) ? data.data[0] : undefined;
2778
- const b64Json = typeof imageData?.b64_json === "string" ? imageData.b64_json : null;
2779
- if (b64Json) {
2780
- return {
2781
- bytes: Buffer.from(b64Json, "base64"),
2782
- contentType: "image/png",
2783
- revisedPrompt: readOptionalStringField(imageData, "revised_prompt") ?? readStringField(data, "revised_prompt")
2784
- };
2785
- }
2786
- const url = readOptionalStringField(imageData, "url") ??
2787
- readOptionalStringField(imageData, "image_url") ??
2788
- readLegacyOpenRouterChatImageUrl(data);
2789
- if (!url) {
2790
- return null;
2791
- }
2792
- const inline = decodeInlineDataUrl(url);
2793
- if (inline) {
2794
- return {
2795
- bytes: inline.bytes,
2796
- contentType: inline.contentType,
2797
- revisedPrompt: readOptionalStringField(imageData, "revised_prompt") ?? readLegacyOpenRouterChatContent(data)
2798
- };
2799
- }
2800
- if (/^https?:\/\//i.test(url)) {
2801
- const { bytes, mimeType } = await fetchRemoteBytes({ url, fallbackMimeType: "image/png" });
2802
- return {
2803
- bytes,
2804
- contentType: mimeType,
2805
- revisedPrompt: readOptionalStringField(imageData, "revised_prompt") ?? readLegacyOpenRouterChatContent(data)
2806
- };
2807
- }
2808
- return null;
2809
- }
2810
- function readOptionalStringField(value, field) {
2811
- return value ? readStringField(value, field) : null;
2812
- }
2813
- function readLegacyOpenRouterChatImageUrl(data) {
2814
- const choices = Array.isArray(data.choices) ? data.choices : [];
2815
- const choice = choices[0];
2816
- const message = choice?.message && typeof choice.message === "object"
2817
- ? choice.message
2818
- : null;
2819
- const images = Array.isArray(message?.images) ? message.images : [];
2820
- const firstImage = images[0];
2821
- const snake = firstImage?.image_url && typeof firstImage.image_url === "object"
2822
- ? firstImage.image_url
2823
- : null;
2824
- const camel = firstImage?.imageUrl && typeof firstImage.imageUrl === "object"
2825
- ? firstImage.imageUrl
2826
- : null;
2827
- return (snake ? readStringField(snake, "url") : null) ?? (camel ? readStringField(camel, "url") : null);
2828
- }
2829
- function readLegacyOpenRouterChatContent(data) {
2830
- const choices = Array.isArray(data.choices) ? data.choices : [];
2831
- const choice = choices[0];
2832
- const message = choice?.message && typeof choice.message === "object"
2833
- ? choice.message
2834
- : null;
2835
- return message ? readStringField(message, "content") : null;
2836
- }
2837
- function conciseOpenRouterImageResponse(data) {
2838
- const message = readLegacyOpenRouterChatContent(data);
2839
- if (message) {
2840
- return message.replace(/\s+/g, " ").trim().slice(0, 500);
2841
- }
2842
- return JSON.stringify(data).replace(/\s+/g, " ").slice(0, 500);
2843
- }
2844
- function buildMockSlideSvg(prompt) {
2845
- const escaped = escapeXml(prompt.slice(0, 220));
2846
- return `
2847
- <svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1920" viewBox="0 0 1080 1920">
2848
- <defs>
2849
- <linearGradient id="bg" x1="0" x2="1" y1="0" y2="1">
2850
- <stop offset="0%" stop-color="#31241c" />
2851
- <stop offset="52%" stop-color="#866245" />
2852
- <stop offset="100%" stop-color="#d7c0a4" />
2853
- </linearGradient>
2854
- </defs>
2855
- <rect width="1080" height="1920" fill="url(#bg)" />
2856
- <rect x="94" y="1180" width="892" height="470" rx="44" fill="rgba(0,0,0,0.18)" stroke="rgba(255,255,255,0.25)" />
2857
- <text x="108" y="170" fill="#f7f0e7" font-size="54" font-family="Times New Roman, serif">Mock AI image</text>
2858
- <text x="108" y="240" fill="#f7f0e7" font-size="34" font-family="Times New Roman, serif">${escaped}</text>
2859
- <text x="108" y="1280" fill="#f7f0e7" font-size="42" font-family="Times New Roman, serif">Reserved text-safe zone</text>
2860
- </svg>`;
2861
- }
2862
- function escapeXml(value) {
2863
- return value
2864
- .replace(/&/g, "&amp;")
2865
- .replace(/</g, "&lt;")
2866
- .replace(/>/g, "&gt;")
2867
- .replace(/"/g, "&quot;")
2868
- .replace(/'/g, "&apos;");
2869
- }
2870
- //# sourceMappingURL=providers.js.map