@stackfactor/agent-utils 1.3.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -49,6 +49,7 @@ declare const _default: {
49
49
  runAgent: (agent: any, prompt: string, config: any, onProgress?: Function | null, usageTracker?: UsageTracker | null) => Promise<any>;
50
50
  runPromptWithModel: (modelName: string, config: any, prompt: any, onProgressReport: any, minPercent?: number, maxPercent?: number, expectsJsonResponse?: boolean, schema?: any, agentName?: string, tools?: any[], usageTracker?: UsageTracker | null) => Promise<any>;
51
51
  runPromptWithModelForImageGeneration: (modelName: string, config: any, prompt: string, options?: any, usageTracker?: UsageTracker | null) => Promise<any>;
52
+ runPromptWithModelForVideoGeneration: (modelName: string, config: any, prompt: string, options?: any, usageTracker?: UsageTracker | null) => Promise<any>;
52
53
  throwErrorIfNotSuccessful: (response: any) => string;
53
54
  updateUsageTrackerForCharacters: (tracker: UsageTracker | null | undefined, modelName: string, characterCount: number, config: any) => void;
54
55
  validateModel: (selectedModel: string, supportedModels: string[]) => string;
@@ -1 +1 @@
1
- {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":"AA4GA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACvC;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACtD,CAAC;AA2lBF;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,eAAO,MAAM,WAAW,GACtB,WAAW,MAAM,EACjB,QAAQ,GAAG,EACX,SAAQ,GAAU,KACjB,GAoHF,CAAC;;0CAzuB2C,GAAG,KAAG,IAAI;wBA4vB/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,iBACG,YAAY,GAAG,IAAI,KAChC,GAAG;sBAoCG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,iBACb,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;oCAkYF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDA2tBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CArgC8B,GAAG,KAAG,MAAM;+CAzuB9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCAyvBU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AA4iCT,wBASE"}
1
+ {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":"AA4GA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACvC;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACtD,CAAC;AAkoBF;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,eAAO,MAAM,WAAW,GACtB,WAAW,MAAM,EACjB,QAAQ,GAAG,EACX,SAAQ,GAAU,KACjB,GAoHF,CAAC;;0CAhxB2C,GAAG,KAAG,IAAI;wBAmyB/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,iBACG,YAAY,GAAG,IAAI,KAChC,GAAG;sBAoCG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,iBACb,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;oCAkYF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDA2tBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDAmTF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CA7zC8B,GAAG,KAAG,MAAM;+CA/wB9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCA+xBU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAg2CT,wBAUE"}
@@ -251,6 +251,39 @@ const updateUsageTrackerForCharacters = (tracker, modelName, characterCount, con
251
251
  const key = `${modelName}_characters`;
252
252
  tracker.tokens[key] = (tracker.tokens[key] || 0) + characterCount;
253
253
  };
254
+ /**
255
+ * Adds second-billed usage (video generation) to the caller-supplied tracker.
256
+ *
257
+ * Video is the first output here that providers bill by DURATION rather than by
258
+ * tokens or characters, so the rate constant `<model>-second-costs` is read as
259
+ * USD PER SECOND — not per million, as the `input-token` and `character` rates
260
+ * are. A Veo or Sora second costs cents, and a per-million reading would bill
261
+ * it as effectively free.
262
+ *
263
+ * The rate is per model, not per model-and-resolution, so a caller that lets a
264
+ * tenant change resolution on a model whose price varies by it (sora-2-pro is
265
+ * $0.30/$0.50/$0.70 at 720p/1024p/1080p) must pin the resolution its constant
266
+ * describes, or it will bill the wrong number.
267
+ *
268
+ * @param tracker - The accumulator to add to; no-ops when null
269
+ * @param modelName - The video model id, used for both the rate and the counter key
270
+ * @param seconds - Billed duration of the generated clip
271
+ * @param config - Integration config carrying `<model>-second-costs`
272
+ */
273
+ const updateVideoUsageTracker = (tracker, modelName, seconds, config) => {
274
+ if (!tracker || !modelName || !seconds)
275
+ return;
276
+ if (typeof tracker.cost !== "number")
277
+ tracker.cost = 0;
278
+ if (!tracker.tokens || typeof tracker.tokens !== "object")
279
+ tracker.tokens = {};
280
+ const rate = getModelRate(modelName, config, "second");
281
+ const addedCost = seconds * rate;
282
+ if (Number.isFinite(addedCost) && addedCost > 0)
283
+ tracker.cost += addedCost;
284
+ const key = `${modelName}_seconds`;
285
+ tracker.tokens[key] = (tracker.tokens[key] || 0) + seconds;
286
+ };
254
287
  /**
255
288
  * Extracts a normalized token-usage object from a single LangChain `invoke()`
256
289
  * response. Reads from `usage_metadata` first (standardized in LangChain v1),
@@ -1771,12 +1804,202 @@ const runPromptWithModelForImageGeneration = async (modelName, config, prompt, o
1771
1804
  throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `Image generation failed: ${error?.message || String(error)}`);
1772
1805
  }
1773
1806
  };
1807
+ /**
1808
+ * Determines the video generation provider for a model name from its prefix.
1809
+ * `veo-` maps to `"google"`, `sora-` to `"openai"`. Returns `null` for anything
1810
+ * else so the caller can reject an unrecognised id rather than guess at it.
1811
+ * @param modelName - The video model identifier to inspect
1812
+ * @returns `"openai"`, `"google"`, or `null`
1813
+ */
1814
+ const getVideoModelProvider = (modelName) => {
1815
+ if (modelName.startsWith("sora-"))
1816
+ return "openai";
1817
+ if (modelName.startsWith("veo-"))
1818
+ return "google";
1819
+ return null;
1820
+ };
1821
+ /** How long to wait between polls of a video generation job. */
1822
+ const VIDEO_POLL_INTERVAL_MS = 10_000;
1823
+ /** How long to keep polling before giving up on a video generation job. */
1824
+ const VIDEO_POLL_TIMEOUT_MS = 600_000;
1825
+ /**
1826
+ * Sleeps for the given number of milliseconds.
1827
+ * @param ms - Milliseconds to wait
1828
+ */
1829
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1830
+ /**
1831
+ * Generates a video with Google Veo.
1832
+ *
1833
+ * Video generation is a long-running operation on both providers — a Veo clip
1834
+ * takes on the order of a minute or two, far past any request timeout — so the
1835
+ * SDK returns an operation handle and this polls it to completion.
1836
+ *
1837
+ * The finished clip comes back either as inline base64 (`videoBytes`) or as a
1838
+ * `uri` on Google's file service, depending on model and size. The uri is NOT
1839
+ * public: it needs the same API key as the generation call, sent as a header,
1840
+ * which is why this fetches it here rather than handing the caller a link that
1841
+ * would 403 the moment it left the process.
1842
+ *
1843
+ * @param modelName - A Veo model id, e.g. `"veo-3.1-fast-generate-preview"`
1844
+ * @param config - Configuration object; must include `googleAPIKey`
1845
+ * @param prompt - The text prompt describing the video
1846
+ * @param options - `aspectRatio`, `resolution`, `durationSeconds`,
1847
+ * `negativePrompt`, `personGeneration`, `pollTimeoutMs`
1848
+ * @param usageTracker - Accumulator billed per second of generated video
1849
+ * @returns `{ b64_json, mimeType, seconds, model }`
1850
+ */
1851
+ const generateVideoWithGoogle = async (modelName, config, prompt, options = {}, usageTracker = null) => {
1852
+ const { aspectRatio = "16:9", resolution = "720p", durationSeconds = 8, negativePrompt, personGeneration, pollTimeoutMs = VIDEO_POLL_TIMEOUT_MS, } = options;
1853
+ const ai = new genai_1.GoogleGenAI({ apiKey: config.googleAPIKey });
1854
+ let operation = await ai.models.generateVideos({
1855
+ model: modelName,
1856
+ prompt,
1857
+ config: {
1858
+ aspectRatio,
1859
+ resolution,
1860
+ durationSeconds,
1861
+ numberOfVideos: 1,
1862
+ ...(negativePrompt ? { negativePrompt } : {}),
1863
+ ...(personGeneration ? { personGeneration } : {}),
1864
+ },
1865
+ });
1866
+ const deadline = Date.now() + pollTimeoutMs;
1867
+ while (!operation.done) {
1868
+ if (Date.now() > deadline) {
1869
+ throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `Video generation with "${modelName}" did not finish within ${Math.round(pollTimeoutMs / 1000)} seconds.`);
1870
+ }
1871
+ await delay(VIDEO_POLL_INTERVAL_MS);
1872
+ operation = await ai.operations.getVideosOperation({ operation });
1873
+ }
1874
+ if (operation.error) {
1875
+ throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `Video generation with "${modelName}" failed: ${operation.error?.message || JSON.stringify(operation.error)}`);
1876
+ }
1877
+ const video = operation.response?.generatedVideos?.[0]?.video;
1878
+ if (!video) {
1879
+ throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `No video was generated by ${modelName}`);
1880
+ }
1881
+ let b64 = video.videoBytes;
1882
+ if (!b64) {
1883
+ if (!video.uri) {
1884
+ throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `${modelName} returned a video with neither inline bytes nor a uri`);
1885
+ }
1886
+ const download = await fetch(video.uri, {
1887
+ headers: { "x-goog-api-key": config.googleAPIKey },
1888
+ });
1889
+ if (!download.ok) {
1890
+ throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `Downloading the generated video failed with ${download.status} ${download.statusText}`);
1891
+ }
1892
+ b64 = Buffer.from(await download.arrayBuffer()).toString("base64");
1893
+ }
1894
+ // Billed on the duration ASKED FOR: the provider bills the clip it rendered,
1895
+ // and there is no returned duration to read back.
1896
+ updateVideoUsageTracker(usageTracker, modelName, durationSeconds, config);
1897
+ return {
1898
+ b64_json: b64,
1899
+ mimeType: video.mimeType || "video/mp4",
1900
+ seconds: durationSeconds,
1901
+ model: modelName,
1902
+ };
1903
+ };
1904
+ /**
1905
+ * Generates a video with OpenAI Sora.
1906
+ *
1907
+ * Like Veo this is a job rather than a request: `videos.create` returns
1908
+ * immediately with a `queued` job, which is polled until it completes and then
1909
+ * downloaded as bytes.
1910
+ *
1911
+ * @param modelName - A Sora model id, e.g. `"sora-2"`
1912
+ * @param config - Configuration object; must include `openAIAPIKey`
1913
+ * @param prompt - The text prompt describing the video
1914
+ * @param options - `size` (`"1280x720"` etc.), `durationSeconds` (4, 8 or 12),
1915
+ * `pollTimeoutMs`
1916
+ * @param usageTracker - Accumulator billed per second of generated video
1917
+ * @returns `{ b64_json, mimeType, seconds, model }`
1918
+ */
1919
+ const generateVideoWithOpenAI = async (modelName, config, prompt, options = {}, usageTracker = null) => {
1920
+ const { size = "1280x720", durationSeconds = 8, pollTimeoutMs = VIDEO_POLL_TIMEOUT_MS, } = options;
1921
+ const openai = new openai_2.OpenAI({ apiKey: config.openAIAPIKey });
1922
+ // Sora takes the duration as one of a fixed set of STRINGS ("4", "8", "12").
1923
+ // A number, or any other value, is rejected by the API.
1924
+ const seconds = String(durationSeconds);
1925
+ if (!["4", "8", "12"].includes(seconds)) {
1926
+ throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, `Invalid durationSeconds "${durationSeconds}" for ${modelName}. Sora supports 4, 8 or 12.`);
1927
+ }
1928
+ let job = await openai.videos.create({
1929
+ model: modelName,
1930
+ prompt,
1931
+ seconds: seconds,
1932
+ size: size,
1933
+ });
1934
+ const deadline = Date.now() + pollTimeoutMs;
1935
+ while (job.status === "queued" || job.status === "in_progress") {
1936
+ if (Date.now() > deadline) {
1937
+ throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `Video generation with "${modelName}" did not finish within ${Math.round(pollTimeoutMs / 1000)} seconds.`);
1938
+ }
1939
+ await delay(VIDEO_POLL_INTERVAL_MS);
1940
+ job = await openai.videos.retrieve(job.id);
1941
+ }
1942
+ if (job.status !== "completed") {
1943
+ throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `Video generation with "${modelName}" ${job.status}: ${job.error?.message || "no reason given"}`);
1944
+ }
1945
+ const content = await openai.videos.downloadContent(job.id);
1946
+ const b64 = Buffer.from(await content.arrayBuffer()).toString("base64");
1947
+ updateVideoUsageTracker(usageTracker, modelName, Number(job.seconds) || Number(seconds), config);
1948
+ return {
1949
+ b64_json: b64,
1950
+ mimeType: "video/mp4",
1951
+ seconds: Number(job.seconds) || Number(seconds),
1952
+ model: modelName,
1953
+ };
1954
+ };
1955
+ /**
1956
+ * Top-level entry point for AI video generation, mirroring
1957
+ * `runPromptWithModelForImageGeneration`. Routes to Veo or Sora on the model
1958
+ * name prefix and logs the total generation time on success.
1959
+ *
1960
+ * Unlike image generation this always returns bytes — never a url. Both
1961
+ * providers hand back a link that requires their API key, so a url would be
1962
+ * useless to any caller that is not this process.
1963
+ *
1964
+ * @param modelName - The video model identifier; must start with `veo-` or `sora-`
1965
+ * @param config - Configuration object with `googleAPIKey` or `openAIAPIKey`
1966
+ * @param prompt - The text prompt describing the video to generate
1967
+ * @param options - Provider-specific generation options
1968
+ * @param usageTracker - Accumulator billed per second of generated video
1969
+ * @returns `{ b64_json, mimeType, seconds, model }`
1970
+ */
1971
+ const runPromptWithModelForVideoGeneration = async (modelName, config, prompt, options = {}, usageTracker = null) => {
1972
+ const provider = getVideoModelProvider(modelName);
1973
+ if (!provider) {
1974
+ throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, `Unable to determine provider for model: ${modelName}. Model name should start with 'veo-' or 'sora-'.`);
1975
+ }
1976
+ const startTime = Date.now();
1977
+ try {
1978
+ const result = provider === "openai"
1979
+ ? await generateVideoWithOpenAI(modelName, config, prompt, options, usageTracker)
1980
+ : await generateVideoWithGoogle(modelName, config, prompt, options, usageTracker);
1981
+ const duration = Date.now() - startTime;
1982
+ logger_js_1.default.log(null, logger_js_1.default.levels.info, `Video generation with "${modelName}" completed in ${Math.round(duration / 1000)} seconds.`);
1983
+ return result;
1984
+ }
1985
+ catch (error) {
1986
+ if (error?.code && error?.message) {
1987
+ throw error;
1988
+ }
1989
+ if (error?.status || error?.code) {
1990
+ const statusCode = error.status || const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR;
1991
+ throw errorHandling_js_1.default.create(statusCode, error.message || "Video generation failed");
1992
+ }
1993
+ throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `Video generation failed: ${error?.message || String(error)}`);
1994
+ }
1995
+ };
1774
1996
  exports.default = {
1775
1997
  checkIfAIProviderConfigured,
1776
1998
  createAgent,
1777
1999
  runAgent,
1778
2000
  runPromptWithModel,
1779
2001
  runPromptWithModelForImageGeneration,
2002
+ runPromptWithModelForVideoGeneration,
1780
2003
  throwErrorIfNotSuccessful,
1781
2004
  updateUsageTrackerForCharacters,
1782
2005
  validateModel,
@@ -49,6 +49,7 @@ declare const _default: {
49
49
  runAgent: (agent: any, prompt: string, config: any, onProgress?: Function | null, usageTracker?: UsageTracker | null) => Promise<any>;
50
50
  runPromptWithModel: (modelName: string, config: any, prompt: any, onProgressReport: any, minPercent?: number, maxPercent?: number, expectsJsonResponse?: boolean, schema?: any, agentName?: string, tools?: any[], usageTracker?: UsageTracker | null) => Promise<any>;
51
51
  runPromptWithModelForImageGeneration: (modelName: string, config: any, prompt: string, options?: any, usageTracker?: UsageTracker | null) => Promise<any>;
52
+ runPromptWithModelForVideoGeneration: (modelName: string, config: any, prompt: string, options?: any, usageTracker?: UsageTracker | null) => Promise<any>;
52
53
  throwErrorIfNotSuccessful: (response: any) => string;
53
54
  updateUsageTrackerForCharacters: (tracker: UsageTracker | null | undefined, modelName: string, characterCount: number, config: any) => void;
54
55
  validateModel: (selectedModel: string, supportedModels: string[]) => string;
@@ -1 +1 @@
1
- {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":"AA4GA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACvC;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACtD,CAAC;AA2lBF;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,eAAO,MAAM,WAAW,GACtB,WAAW,MAAM,EACjB,QAAQ,GAAG,EACX,SAAQ,GAAU,KACjB,GAoHF,CAAC;;0CAzuB2C,GAAG,KAAG,IAAI;wBA4vB/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,iBACG,YAAY,GAAG,IAAI,KAChC,GAAG;sBAoCG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,iBACb,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;oCAkYF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDA2tBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CArgC8B,GAAG,KAAG,MAAM;+CAzuB9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCAyvBU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AA4iCT,wBASE"}
1
+ {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":"AA4GA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACvC;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACtD,CAAC;AAkoBF;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,eAAO,MAAM,WAAW,GACtB,WAAW,MAAM,EACjB,QAAQ,GAAG,EACX,SAAQ,GAAU,KACjB,GAoHF,CAAC;;0CAhxB2C,GAAG,KAAG,IAAI;wBAmyB/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,iBACG,YAAY,GAAG,IAAI,KAChC,GAAG;sBAoCG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,iBACb,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;oCAkYF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDA2tBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDAmTF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CA7zC8B,GAAG,KAAG,MAAM;+CA/wB9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCA+xBU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAg2CT,wBAUE"}
@@ -245,6 +245,39 @@ const updateUsageTrackerForCharacters = (tracker, modelName, characterCount, con
245
245
  const key = `${modelName}_characters`;
246
246
  tracker.tokens[key] = (tracker.tokens[key] || 0) + characterCount;
247
247
  };
248
+ /**
249
+ * Adds second-billed usage (video generation) to the caller-supplied tracker.
250
+ *
251
+ * Video is the first output here that providers bill by DURATION rather than by
252
+ * tokens or characters, so the rate constant `<model>-second-costs` is read as
253
+ * USD PER SECOND — not per million, as the `input-token` and `character` rates
254
+ * are. A Veo or Sora second costs cents, and a per-million reading would bill
255
+ * it as effectively free.
256
+ *
257
+ * The rate is per model, not per model-and-resolution, so a caller that lets a
258
+ * tenant change resolution on a model whose price varies by it (sora-2-pro is
259
+ * $0.30/$0.50/$0.70 at 720p/1024p/1080p) must pin the resolution its constant
260
+ * describes, or it will bill the wrong number.
261
+ *
262
+ * @param tracker - The accumulator to add to; no-ops when null
263
+ * @param modelName - The video model id, used for both the rate and the counter key
264
+ * @param seconds - Billed duration of the generated clip
265
+ * @param config - Integration config carrying `<model>-second-costs`
266
+ */
267
+ const updateVideoUsageTracker = (tracker, modelName, seconds, config) => {
268
+ if (!tracker || !modelName || !seconds)
269
+ return;
270
+ if (typeof tracker.cost !== "number")
271
+ tracker.cost = 0;
272
+ if (!tracker.tokens || typeof tracker.tokens !== "object")
273
+ tracker.tokens = {};
274
+ const rate = getModelRate(modelName, config, "second");
275
+ const addedCost = seconds * rate;
276
+ if (Number.isFinite(addedCost) && addedCost > 0)
277
+ tracker.cost += addedCost;
278
+ const key = `${modelName}_seconds`;
279
+ tracker.tokens[key] = (tracker.tokens[key] || 0) + seconds;
280
+ };
248
281
  /**
249
282
  * Extracts a normalized token-usage object from a single LangChain `invoke()`
250
283
  * response. Reads from `usage_metadata` first (standardized in LangChain v1),
@@ -1764,12 +1797,202 @@ const runPromptWithModelForImageGeneration = async (modelName, config, prompt, o
1764
1797
  throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `Image generation failed: ${error?.message || String(error)}`);
1765
1798
  }
1766
1799
  };
1800
+ /**
1801
+ * Determines the video generation provider for a model name from its prefix.
1802
+ * `veo-` maps to `"google"`, `sora-` to `"openai"`. Returns `null` for anything
1803
+ * else so the caller can reject an unrecognised id rather than guess at it.
1804
+ * @param modelName - The video model identifier to inspect
1805
+ * @returns `"openai"`, `"google"`, or `null`
1806
+ */
1807
+ const getVideoModelProvider = (modelName) => {
1808
+ if (modelName.startsWith("sora-"))
1809
+ return "openai";
1810
+ if (modelName.startsWith("veo-"))
1811
+ return "google";
1812
+ return null;
1813
+ };
1814
+ /** How long to wait between polls of a video generation job. */
1815
+ const VIDEO_POLL_INTERVAL_MS = 10_000;
1816
+ /** How long to keep polling before giving up on a video generation job. */
1817
+ const VIDEO_POLL_TIMEOUT_MS = 600_000;
1818
+ /**
1819
+ * Sleeps for the given number of milliseconds.
1820
+ * @param ms - Milliseconds to wait
1821
+ */
1822
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1823
+ /**
1824
+ * Generates a video with Google Veo.
1825
+ *
1826
+ * Video generation is a long-running operation on both providers — a Veo clip
1827
+ * takes on the order of a minute or two, far past any request timeout — so the
1828
+ * SDK returns an operation handle and this polls it to completion.
1829
+ *
1830
+ * The finished clip comes back either as inline base64 (`videoBytes`) or as a
1831
+ * `uri` on Google's file service, depending on model and size. The uri is NOT
1832
+ * public: it needs the same API key as the generation call, sent as a header,
1833
+ * which is why this fetches it here rather than handing the caller a link that
1834
+ * would 403 the moment it left the process.
1835
+ *
1836
+ * @param modelName - A Veo model id, e.g. `"veo-3.1-fast-generate-preview"`
1837
+ * @param config - Configuration object; must include `googleAPIKey`
1838
+ * @param prompt - The text prompt describing the video
1839
+ * @param options - `aspectRatio`, `resolution`, `durationSeconds`,
1840
+ * `negativePrompt`, `personGeneration`, `pollTimeoutMs`
1841
+ * @param usageTracker - Accumulator billed per second of generated video
1842
+ * @returns `{ b64_json, mimeType, seconds, model }`
1843
+ */
1844
+ const generateVideoWithGoogle = async (modelName, config, prompt, options = {}, usageTracker = null) => {
1845
+ const { aspectRatio = "16:9", resolution = "720p", durationSeconds = 8, negativePrompt, personGeneration, pollTimeoutMs = VIDEO_POLL_TIMEOUT_MS, } = options;
1846
+ const ai = new GoogleGenAI({ apiKey: config.googleAPIKey });
1847
+ let operation = await ai.models.generateVideos({
1848
+ model: modelName,
1849
+ prompt,
1850
+ config: {
1851
+ aspectRatio,
1852
+ resolution,
1853
+ durationSeconds,
1854
+ numberOfVideos: 1,
1855
+ ...(negativePrompt ? { negativePrompt } : {}),
1856
+ ...(personGeneration ? { personGeneration } : {}),
1857
+ },
1858
+ });
1859
+ const deadline = Date.now() + pollTimeoutMs;
1860
+ while (!operation.done) {
1861
+ if (Date.now() > deadline) {
1862
+ throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `Video generation with "${modelName}" did not finish within ${Math.round(pollTimeoutMs / 1000)} seconds.`);
1863
+ }
1864
+ await delay(VIDEO_POLL_INTERVAL_MS);
1865
+ operation = await ai.operations.getVideosOperation({ operation });
1866
+ }
1867
+ if (operation.error) {
1868
+ throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `Video generation with "${modelName}" failed: ${operation.error?.message || JSON.stringify(operation.error)}`);
1869
+ }
1870
+ const video = operation.response?.generatedVideos?.[0]?.video;
1871
+ if (!video) {
1872
+ throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `No video was generated by ${modelName}`);
1873
+ }
1874
+ let b64 = video.videoBytes;
1875
+ if (!b64) {
1876
+ if (!video.uri) {
1877
+ throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `${modelName} returned a video with neither inline bytes nor a uri`);
1878
+ }
1879
+ const download = await fetch(video.uri, {
1880
+ headers: { "x-goog-api-key": config.googleAPIKey },
1881
+ });
1882
+ if (!download.ok) {
1883
+ throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `Downloading the generated video failed with ${download.status} ${download.statusText}`);
1884
+ }
1885
+ b64 = Buffer.from(await download.arrayBuffer()).toString("base64");
1886
+ }
1887
+ // Billed on the duration ASKED FOR: the provider bills the clip it rendered,
1888
+ // and there is no returned duration to read back.
1889
+ updateVideoUsageTracker(usageTracker, modelName, durationSeconds, config);
1890
+ return {
1891
+ b64_json: b64,
1892
+ mimeType: video.mimeType || "video/mp4",
1893
+ seconds: durationSeconds,
1894
+ model: modelName,
1895
+ };
1896
+ };
1897
+ /**
1898
+ * Generates a video with OpenAI Sora.
1899
+ *
1900
+ * Like Veo this is a job rather than a request: `videos.create` returns
1901
+ * immediately with a `queued` job, which is polled until it completes and then
1902
+ * downloaded as bytes.
1903
+ *
1904
+ * @param modelName - A Sora model id, e.g. `"sora-2"`
1905
+ * @param config - Configuration object; must include `openAIAPIKey`
1906
+ * @param prompt - The text prompt describing the video
1907
+ * @param options - `size` (`"1280x720"` etc.), `durationSeconds` (4, 8 or 12),
1908
+ * `pollTimeoutMs`
1909
+ * @param usageTracker - Accumulator billed per second of generated video
1910
+ * @returns `{ b64_json, mimeType, seconds, model }`
1911
+ */
1912
+ const generateVideoWithOpenAI = async (modelName, config, prompt, options = {}, usageTracker = null) => {
1913
+ const { size = "1280x720", durationSeconds = 8, pollTimeoutMs = VIDEO_POLL_TIMEOUT_MS, } = options;
1914
+ const openai = new OpenAI({ apiKey: config.openAIAPIKey });
1915
+ // Sora takes the duration as one of a fixed set of STRINGS ("4", "8", "12").
1916
+ // A number, or any other value, is rejected by the API.
1917
+ const seconds = String(durationSeconds);
1918
+ if (!["4", "8", "12"].includes(seconds)) {
1919
+ throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, `Invalid durationSeconds "${durationSeconds}" for ${modelName}. Sora supports 4, 8 or 12.`);
1920
+ }
1921
+ let job = await openai.videos.create({
1922
+ model: modelName,
1923
+ prompt,
1924
+ seconds: seconds,
1925
+ size: size,
1926
+ });
1927
+ const deadline = Date.now() + pollTimeoutMs;
1928
+ while (job.status === "queued" || job.status === "in_progress") {
1929
+ if (Date.now() > deadline) {
1930
+ throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `Video generation with "${modelName}" did not finish within ${Math.round(pollTimeoutMs / 1000)} seconds.`);
1931
+ }
1932
+ await delay(VIDEO_POLL_INTERVAL_MS);
1933
+ job = await openai.videos.retrieve(job.id);
1934
+ }
1935
+ if (job.status !== "completed") {
1936
+ throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `Video generation with "${modelName}" ${job.status}: ${job.error?.message || "no reason given"}`);
1937
+ }
1938
+ const content = await openai.videos.downloadContent(job.id);
1939
+ const b64 = Buffer.from(await content.arrayBuffer()).toString("base64");
1940
+ updateVideoUsageTracker(usageTracker, modelName, Number(job.seconds) || Number(seconds), config);
1941
+ return {
1942
+ b64_json: b64,
1943
+ mimeType: "video/mp4",
1944
+ seconds: Number(job.seconds) || Number(seconds),
1945
+ model: modelName,
1946
+ };
1947
+ };
1948
+ /**
1949
+ * Top-level entry point for AI video generation, mirroring
1950
+ * `runPromptWithModelForImageGeneration`. Routes to Veo or Sora on the model
1951
+ * name prefix and logs the total generation time on success.
1952
+ *
1953
+ * Unlike image generation this always returns bytes — never a url. Both
1954
+ * providers hand back a link that requires their API key, so a url would be
1955
+ * useless to any caller that is not this process.
1956
+ *
1957
+ * @param modelName - The video model identifier; must start with `veo-` or `sora-`
1958
+ * @param config - Configuration object with `googleAPIKey` or `openAIAPIKey`
1959
+ * @param prompt - The text prompt describing the video to generate
1960
+ * @param options - Provider-specific generation options
1961
+ * @param usageTracker - Accumulator billed per second of generated video
1962
+ * @returns `{ b64_json, mimeType, seconds, model }`
1963
+ */
1964
+ const runPromptWithModelForVideoGeneration = async (modelName, config, prompt, options = {}, usageTracker = null) => {
1965
+ const provider = getVideoModelProvider(modelName);
1966
+ if (!provider) {
1967
+ throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, `Unable to determine provider for model: ${modelName}. Model name should start with 'veo-' or 'sora-'.`);
1968
+ }
1969
+ const startTime = Date.now();
1970
+ try {
1971
+ const result = provider === "openai"
1972
+ ? await generateVideoWithOpenAI(modelName, config, prompt, options, usageTracker)
1973
+ : await generateVideoWithGoogle(modelName, config, prompt, options, usageTracker);
1974
+ const duration = Date.now() - startTime;
1975
+ logger.log(null, logger.levels.info, `Video generation with "${modelName}" completed in ${Math.round(duration / 1000)} seconds.`);
1976
+ return result;
1977
+ }
1978
+ catch (error) {
1979
+ if (error?.code && error?.message) {
1980
+ throw error;
1981
+ }
1982
+ if (error?.status || error?.code) {
1983
+ const statusCode = error.status || constants.HTTP_CODES.INTERNAL_SERVER_ERROR;
1984
+ throw errorHandlingHelper.create(statusCode, error.message || "Video generation failed");
1985
+ }
1986
+ throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `Video generation failed: ${error?.message || String(error)}`);
1987
+ }
1988
+ };
1767
1989
  export default {
1768
1990
  checkIfAIProviderConfigured,
1769
1991
  createAgent,
1770
1992
  runAgent,
1771
1993
  runPromptWithModel,
1772
1994
  runPromptWithModelForImageGeneration,
1995
+ runPromptWithModelForVideoGeneration,
1773
1996
  throwErrorIfNotSuccessful,
1774
1997
  updateUsageTrackerForCharacters,
1775
1998
  validateModel,
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.4",
6
+ "version": "1.4.0",
7
7
  "description": "",
8
8
  "main": "dist/cjs/index.js",
9
9
  "module": "dist/esm/index.js",