@stackfactor/agent-utils 1.3.3 → 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;sDA0tBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CApgC8B,GAAG,KAAG,MAAM;+CAzuB9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCAyvBU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AA2iCT,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),
@@ -1336,6 +1369,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1336
1369
  const abortSignal = (0, runtimeContext_js_1.getAbortSignal)();
1337
1370
  const stream = await llm.stream(activeMessages, {
1338
1371
  signal: abortSignal ?? undefined,
1372
+ runName: agentName,
1339
1373
  });
1340
1374
  for await (const chunk of stream) {
1341
1375
  if (abortSignal?.aborted) {
@@ -1461,7 +1495,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1461
1495
  let rateLimitAttempt = 0;
1462
1496
  while (true) {
1463
1497
  try {
1464
- response = await llm.invoke(activeMessages);
1498
+ response = await llm.invoke(activeMessages, { runName: agentName });
1465
1499
  break;
1466
1500
  }
1467
1501
  catch (err) {
@@ -1770,12 +1804,202 @@ const runPromptWithModelForImageGeneration = async (modelName, config, prompt, o
1770
1804
  throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `Image generation failed: ${error?.message || String(error)}`);
1771
1805
  }
1772
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
+ };
1773
1996
  exports.default = {
1774
1997
  checkIfAIProviderConfigured,
1775
1998
  createAgent,
1776
1999
  runAgent,
1777
2000
  runPromptWithModel,
1778
2001
  runPromptWithModelForImageGeneration,
2002
+ runPromptWithModelForVideoGeneration,
1779
2003
  throwErrorIfNotSuccessful,
1780
2004
  updateUsageTrackerForCharacters,
1781
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;sDA0tBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CApgC8B,GAAG,KAAG,MAAM;+CAzuB9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCAyvBU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AA2iCT,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),
@@ -1329,6 +1362,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1329
1362
  const abortSignal = getAbortSignal();
1330
1363
  const stream = await llm.stream(activeMessages, {
1331
1364
  signal: abortSignal ?? undefined,
1365
+ runName: agentName,
1332
1366
  });
1333
1367
  for await (const chunk of stream) {
1334
1368
  if (abortSignal?.aborted) {
@@ -1454,7 +1488,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1454
1488
  let rateLimitAttempt = 0;
1455
1489
  while (true) {
1456
1490
  try {
1457
- response = await llm.invoke(activeMessages);
1491
+ response = await llm.invoke(activeMessages, { runName: agentName });
1458
1492
  break;
1459
1493
  }
1460
1494
  catch (err) {
@@ -1763,12 +1797,202 @@ const runPromptWithModelForImageGeneration = async (modelName, config, prompt, o
1763
1797
  throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `Image generation failed: ${error?.message || String(error)}`);
1764
1798
  }
1765
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
+ };
1766
1989
  export default {
1767
1990
  checkIfAIProviderConfigured,
1768
1991
  createAgent,
1769
1992
  runAgent,
1770
1993
  runPromptWithModel,
1771
1994
  runPromptWithModelForImageGeneration,
1995
+ runPromptWithModelForVideoGeneration,
1772
1996
  throwErrorIfNotSuccessful,
1773
1997
  updateUsageTrackerForCharacters,
1774
1998
  validateModel,
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.3",
6
+ "version": "1.4.0",
7
7
  "description": "",
8
8
  "main": "dist/cjs/index.js",
9
9
  "module": "dist/esm/index.js",