@stackfactor/agent-utils 1.3.4 → 1.4.1

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;AAmrBF;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,eAAO,MAAM,WAAW,GACtB,WAAW,MAAM,EACjB,QAAQ,GAAG,EACX,SAAQ,GAAU,KACjB,GAoHF,CAAC;;0CAj0B2C,GAAG,KAAG,IAAI;wBAo1B/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;+CAh0B9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCAg1BU,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),
@@ -610,6 +643,46 @@ const resolveTemperatureSetting = (modelName, config) => {
610
643
  }
611
644
  return { temperature: clamped };
612
645
  };
646
+ /**
647
+ * Gemini's ceiling on `maxOutputTokens`. The API validates the field and
648
+ * rejects the request outright rather than clamping it:
649
+ * "Unable to submit request because it has a maxOutputTokens value of N but the
650
+ * supported range is from 1 (inclusive) to 65537 (exclusive)" (HTTP 400,
651
+ * INVALID_ARGUMENT).
652
+ *
653
+ * Set to 64000 rather than that 65536 validation ceiling: the Pro models report
654
+ * a generation capacity of 64000, so 64000 is the value every model across the
655
+ * Gemini 3 line (3.1 Pro, 3.5/3.7/3.8 Flash) can both accept and actually
656
+ * produce. One safe constant beats a per-model table that would need editing
657
+ * every time Google ships a model — and the 1536 tokens of headroom given up on
658
+ * Flash are far beyond what any caller asks for.
659
+ *
660
+ * Deliberately Gemini-only. Anthropic and OpenAI take the caller's `maxTokens`
661
+ * unchanged, since neither rejects the values configured today.
662
+ */
663
+ const GEMINI_MAX_OUTPUT_TOKENS = 64000;
664
+ /**
665
+ * Resolves `maxOutputTokens` for a Gemini model. Callers share one `maxTokens`
666
+ * setting across every provider, and the other providers tolerate values far
667
+ * above what Gemini accepts, so anything above Gemini's ceiling is clamped here
668
+ * (with a warning) instead of being sent through to a guaranteed HTTP 400. With
669
+ * no caller value the ceiling itself is used, which is what the previous
670
+ * 200000 default was reaching for.
671
+ * @param modelName - The Gemini model identifier being routed
672
+ * @param config - Configuration object; only `maxTokens` is consulted
673
+ * @returns A token count within Gemini's supported range
674
+ */
675
+ const resolveGeminiMaxOutputTokens = (modelName, config) => {
676
+ const maxTokens = config?.maxTokens;
677
+ if (typeof maxTokens !== "number" || !Number.isFinite(maxTokens)) {
678
+ return GEMINI_MAX_OUTPUT_TOKENS;
679
+ }
680
+ if (maxTokens > GEMINI_MAX_OUTPUT_TOKENS) {
681
+ logger_js_1.default.log(null, logger_js_1.default.levels.warn, `maxTokens ${maxTokens} exceeds the ${GEMINI_MAX_OUTPUT_TOKENS}-token output ceiling of "${modelName}"; clamping to ${GEMINI_MAX_OUTPUT_TOKENS}.`);
682
+ return GEMINI_MAX_OUTPUT_TOKENS;
683
+ }
684
+ return maxTokens;
685
+ };
613
686
  /**
614
687
  * Flattens LangChain message content into plain text. Content is a string for
615
688
  * ordinary completions, but providers switch to an array of blocks whenever the
@@ -701,7 +774,7 @@ const getLLMModel = (modelName, config, schema = null) => {
701
774
  else if (modelName.startsWith("gemini-")) {
702
775
  const model = new google_genai_1.ChatGoogleGenerativeAI({
703
776
  apiKey: config.googleAPIKey,
704
- maxOutputTokens: config.maxTokens || 200000,
777
+ maxOutputTokens: resolveGeminiMaxOutputTokens(modelName, config),
705
778
  model: modelName,
706
779
  // `json: true` forces responseMimeType=application/json, guaranteeing the
707
780
  // model returns valid JSON text (never prose) for the parse pipeline.
@@ -1771,12 +1844,202 @@ const runPromptWithModelForImageGeneration = async (modelName, config, prompt, o
1771
1844
  throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `Image generation failed: ${error?.message || String(error)}`);
1772
1845
  }
1773
1846
  };
1847
+ /**
1848
+ * Determines the video generation provider for a model name from its prefix.
1849
+ * `veo-` maps to `"google"`, `sora-` to `"openai"`. Returns `null` for anything
1850
+ * else so the caller can reject an unrecognised id rather than guess at it.
1851
+ * @param modelName - The video model identifier to inspect
1852
+ * @returns `"openai"`, `"google"`, or `null`
1853
+ */
1854
+ const getVideoModelProvider = (modelName) => {
1855
+ if (modelName.startsWith("sora-"))
1856
+ return "openai";
1857
+ if (modelName.startsWith("veo-"))
1858
+ return "google";
1859
+ return null;
1860
+ };
1861
+ /** How long to wait between polls of a video generation job. */
1862
+ const VIDEO_POLL_INTERVAL_MS = 10_000;
1863
+ /** How long to keep polling before giving up on a video generation job. */
1864
+ const VIDEO_POLL_TIMEOUT_MS = 600_000;
1865
+ /**
1866
+ * Sleeps for the given number of milliseconds.
1867
+ * @param ms - Milliseconds to wait
1868
+ */
1869
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1870
+ /**
1871
+ * Generates a video with Google Veo.
1872
+ *
1873
+ * Video generation is a long-running operation on both providers — a Veo clip
1874
+ * takes on the order of a minute or two, far past any request timeout — so the
1875
+ * SDK returns an operation handle and this polls it to completion.
1876
+ *
1877
+ * The finished clip comes back either as inline base64 (`videoBytes`) or as a
1878
+ * `uri` on Google's file service, depending on model and size. The uri is NOT
1879
+ * public: it needs the same API key as the generation call, sent as a header,
1880
+ * which is why this fetches it here rather than handing the caller a link that
1881
+ * would 403 the moment it left the process.
1882
+ *
1883
+ * @param modelName - A Veo model id, e.g. `"veo-3.1-fast-generate-preview"`
1884
+ * @param config - Configuration object; must include `googleAPIKey`
1885
+ * @param prompt - The text prompt describing the video
1886
+ * @param options - `aspectRatio`, `resolution`, `durationSeconds`,
1887
+ * `negativePrompt`, `personGeneration`, `pollTimeoutMs`
1888
+ * @param usageTracker - Accumulator billed per second of generated video
1889
+ * @returns `{ b64_json, mimeType, seconds, model }`
1890
+ */
1891
+ const generateVideoWithGoogle = async (modelName, config, prompt, options = {}, usageTracker = null) => {
1892
+ const { aspectRatio = "16:9", resolution = "720p", durationSeconds = 8, negativePrompt, personGeneration, pollTimeoutMs = VIDEO_POLL_TIMEOUT_MS, } = options;
1893
+ const ai = new genai_1.GoogleGenAI({ apiKey: config.googleAPIKey });
1894
+ let operation = await ai.models.generateVideos({
1895
+ model: modelName,
1896
+ prompt,
1897
+ config: {
1898
+ aspectRatio,
1899
+ resolution,
1900
+ durationSeconds,
1901
+ numberOfVideos: 1,
1902
+ ...(negativePrompt ? { negativePrompt } : {}),
1903
+ ...(personGeneration ? { personGeneration } : {}),
1904
+ },
1905
+ });
1906
+ const deadline = Date.now() + pollTimeoutMs;
1907
+ while (!operation.done) {
1908
+ if (Date.now() > deadline) {
1909
+ 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.`);
1910
+ }
1911
+ await delay(VIDEO_POLL_INTERVAL_MS);
1912
+ operation = await ai.operations.getVideosOperation({ operation });
1913
+ }
1914
+ if (operation.error) {
1915
+ 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)}`);
1916
+ }
1917
+ const video = operation.response?.generatedVideos?.[0]?.video;
1918
+ if (!video) {
1919
+ throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `No video was generated by ${modelName}`);
1920
+ }
1921
+ let b64 = video.videoBytes;
1922
+ if (!b64) {
1923
+ if (!video.uri) {
1924
+ 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`);
1925
+ }
1926
+ const download = await fetch(video.uri, {
1927
+ headers: { "x-goog-api-key": config.googleAPIKey },
1928
+ });
1929
+ if (!download.ok) {
1930
+ 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}`);
1931
+ }
1932
+ b64 = Buffer.from(await download.arrayBuffer()).toString("base64");
1933
+ }
1934
+ // Billed on the duration ASKED FOR: the provider bills the clip it rendered,
1935
+ // and there is no returned duration to read back.
1936
+ updateVideoUsageTracker(usageTracker, modelName, durationSeconds, config);
1937
+ return {
1938
+ b64_json: b64,
1939
+ mimeType: video.mimeType || "video/mp4",
1940
+ seconds: durationSeconds,
1941
+ model: modelName,
1942
+ };
1943
+ };
1944
+ /**
1945
+ * Generates a video with OpenAI Sora.
1946
+ *
1947
+ * Like Veo this is a job rather than a request: `videos.create` returns
1948
+ * immediately with a `queued` job, which is polled until it completes and then
1949
+ * downloaded as bytes.
1950
+ *
1951
+ * @param modelName - A Sora model id, e.g. `"sora-2"`
1952
+ * @param config - Configuration object; must include `openAIAPIKey`
1953
+ * @param prompt - The text prompt describing the video
1954
+ * @param options - `size` (`"1280x720"` etc.), `durationSeconds` (4, 8 or 12),
1955
+ * `pollTimeoutMs`
1956
+ * @param usageTracker - Accumulator billed per second of generated video
1957
+ * @returns `{ b64_json, mimeType, seconds, model }`
1958
+ */
1959
+ const generateVideoWithOpenAI = async (modelName, config, prompt, options = {}, usageTracker = null) => {
1960
+ const { size = "1280x720", durationSeconds = 8, pollTimeoutMs = VIDEO_POLL_TIMEOUT_MS, } = options;
1961
+ const openai = new openai_2.OpenAI({ apiKey: config.openAIAPIKey });
1962
+ // Sora takes the duration as one of a fixed set of STRINGS ("4", "8", "12").
1963
+ // A number, or any other value, is rejected by the API.
1964
+ const seconds = String(durationSeconds);
1965
+ if (!["4", "8", "12"].includes(seconds)) {
1966
+ 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.`);
1967
+ }
1968
+ let job = await openai.videos.create({
1969
+ model: modelName,
1970
+ prompt,
1971
+ seconds: seconds,
1972
+ size: size,
1973
+ });
1974
+ const deadline = Date.now() + pollTimeoutMs;
1975
+ while (job.status === "queued" || job.status === "in_progress") {
1976
+ if (Date.now() > deadline) {
1977
+ 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.`);
1978
+ }
1979
+ await delay(VIDEO_POLL_INTERVAL_MS);
1980
+ job = await openai.videos.retrieve(job.id);
1981
+ }
1982
+ if (job.status !== "completed") {
1983
+ 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"}`);
1984
+ }
1985
+ const content = await openai.videos.downloadContent(job.id);
1986
+ const b64 = Buffer.from(await content.arrayBuffer()).toString("base64");
1987
+ updateVideoUsageTracker(usageTracker, modelName, Number(job.seconds) || Number(seconds), config);
1988
+ return {
1989
+ b64_json: b64,
1990
+ mimeType: "video/mp4",
1991
+ seconds: Number(job.seconds) || Number(seconds),
1992
+ model: modelName,
1993
+ };
1994
+ };
1995
+ /**
1996
+ * Top-level entry point for AI video generation, mirroring
1997
+ * `runPromptWithModelForImageGeneration`. Routes to Veo or Sora on the model
1998
+ * name prefix and logs the total generation time on success.
1999
+ *
2000
+ * Unlike image generation this always returns bytes — never a url. Both
2001
+ * providers hand back a link that requires their API key, so a url would be
2002
+ * useless to any caller that is not this process.
2003
+ *
2004
+ * @param modelName - The video model identifier; must start with `veo-` or `sora-`
2005
+ * @param config - Configuration object with `googleAPIKey` or `openAIAPIKey`
2006
+ * @param prompt - The text prompt describing the video to generate
2007
+ * @param options - Provider-specific generation options
2008
+ * @param usageTracker - Accumulator billed per second of generated video
2009
+ * @returns `{ b64_json, mimeType, seconds, model }`
2010
+ */
2011
+ const runPromptWithModelForVideoGeneration = async (modelName, config, prompt, options = {}, usageTracker = null) => {
2012
+ const provider = getVideoModelProvider(modelName);
2013
+ if (!provider) {
2014
+ 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-'.`);
2015
+ }
2016
+ const startTime = Date.now();
2017
+ try {
2018
+ const result = provider === "openai"
2019
+ ? await generateVideoWithOpenAI(modelName, config, prompt, options, usageTracker)
2020
+ : await generateVideoWithGoogle(modelName, config, prompt, options, usageTracker);
2021
+ const duration = Date.now() - startTime;
2022
+ logger_js_1.default.log(null, logger_js_1.default.levels.info, `Video generation with "${modelName}" completed in ${Math.round(duration / 1000)} seconds.`);
2023
+ return result;
2024
+ }
2025
+ catch (error) {
2026
+ if (error?.code && error?.message) {
2027
+ throw error;
2028
+ }
2029
+ if (error?.status || error?.code) {
2030
+ const statusCode = error.status || const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR;
2031
+ throw errorHandling_js_1.default.create(statusCode, error.message || "Video generation failed");
2032
+ }
2033
+ throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `Video generation failed: ${error?.message || String(error)}`);
2034
+ }
2035
+ };
1774
2036
  exports.default = {
1775
2037
  checkIfAIProviderConfigured,
1776
2038
  createAgent,
1777
2039
  runAgent,
1778
2040
  runPromptWithModel,
1779
2041
  runPromptWithModelForImageGeneration,
2042
+ runPromptWithModelForVideoGeneration,
1780
2043
  throwErrorIfNotSuccessful,
1781
2044
  updateUsageTrackerForCharacters,
1782
2045
  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;AAmrBF;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,eAAO,MAAM,WAAW,GACtB,WAAW,MAAM,EACjB,QAAQ,GAAG,EACX,SAAQ,GAAU,KACjB,GAoHF,CAAC;;0CAj0B2C,GAAG,KAAG,IAAI;wBAo1B/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;+CAh0B9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCAg1BU,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),
@@ -604,6 +637,46 @@ const resolveTemperatureSetting = (modelName, config) => {
604
637
  }
605
638
  return { temperature: clamped };
606
639
  };
640
+ /**
641
+ * Gemini's ceiling on `maxOutputTokens`. The API validates the field and
642
+ * rejects the request outright rather than clamping it:
643
+ * "Unable to submit request because it has a maxOutputTokens value of N but the
644
+ * supported range is from 1 (inclusive) to 65537 (exclusive)" (HTTP 400,
645
+ * INVALID_ARGUMENT).
646
+ *
647
+ * Set to 64000 rather than that 65536 validation ceiling: the Pro models report
648
+ * a generation capacity of 64000, so 64000 is the value every model across the
649
+ * Gemini 3 line (3.1 Pro, 3.5/3.7/3.8 Flash) can both accept and actually
650
+ * produce. One safe constant beats a per-model table that would need editing
651
+ * every time Google ships a model — and the 1536 tokens of headroom given up on
652
+ * Flash are far beyond what any caller asks for.
653
+ *
654
+ * Deliberately Gemini-only. Anthropic and OpenAI take the caller's `maxTokens`
655
+ * unchanged, since neither rejects the values configured today.
656
+ */
657
+ const GEMINI_MAX_OUTPUT_TOKENS = 64000;
658
+ /**
659
+ * Resolves `maxOutputTokens` for a Gemini model. Callers share one `maxTokens`
660
+ * setting across every provider, and the other providers tolerate values far
661
+ * above what Gemini accepts, so anything above Gemini's ceiling is clamped here
662
+ * (with a warning) instead of being sent through to a guaranteed HTTP 400. With
663
+ * no caller value the ceiling itself is used, which is what the previous
664
+ * 200000 default was reaching for.
665
+ * @param modelName - The Gemini model identifier being routed
666
+ * @param config - Configuration object; only `maxTokens` is consulted
667
+ * @returns A token count within Gemini's supported range
668
+ */
669
+ const resolveGeminiMaxOutputTokens = (modelName, config) => {
670
+ const maxTokens = config?.maxTokens;
671
+ if (typeof maxTokens !== "number" || !Number.isFinite(maxTokens)) {
672
+ return GEMINI_MAX_OUTPUT_TOKENS;
673
+ }
674
+ if (maxTokens > GEMINI_MAX_OUTPUT_TOKENS) {
675
+ logger.log(null, logger.levels.warn, `maxTokens ${maxTokens} exceeds the ${GEMINI_MAX_OUTPUT_TOKENS}-token output ceiling of "${modelName}"; clamping to ${GEMINI_MAX_OUTPUT_TOKENS}.`);
676
+ return GEMINI_MAX_OUTPUT_TOKENS;
677
+ }
678
+ return maxTokens;
679
+ };
607
680
  /**
608
681
  * Flattens LangChain message content into plain text. Content is a string for
609
682
  * ordinary completions, but providers switch to an array of blocks whenever the
@@ -695,7 +768,7 @@ export const getLLMModel = (modelName, config, schema = null) => {
695
768
  else if (modelName.startsWith("gemini-")) {
696
769
  const model = new ChatGoogleGenerativeAI({
697
770
  apiKey: config.googleAPIKey,
698
- maxOutputTokens: config.maxTokens || 200000,
771
+ maxOutputTokens: resolveGeminiMaxOutputTokens(modelName, config),
699
772
  model: modelName,
700
773
  // `json: true` forces responseMimeType=application/json, guaranteeing the
701
774
  // model returns valid JSON text (never prose) for the parse pipeline.
@@ -1764,12 +1837,202 @@ const runPromptWithModelForImageGeneration = async (modelName, config, prompt, o
1764
1837
  throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `Image generation failed: ${error?.message || String(error)}`);
1765
1838
  }
1766
1839
  };
1840
+ /**
1841
+ * Determines the video generation provider for a model name from its prefix.
1842
+ * `veo-` maps to `"google"`, `sora-` to `"openai"`. Returns `null` for anything
1843
+ * else so the caller can reject an unrecognised id rather than guess at it.
1844
+ * @param modelName - The video model identifier to inspect
1845
+ * @returns `"openai"`, `"google"`, or `null`
1846
+ */
1847
+ const getVideoModelProvider = (modelName) => {
1848
+ if (modelName.startsWith("sora-"))
1849
+ return "openai";
1850
+ if (modelName.startsWith("veo-"))
1851
+ return "google";
1852
+ return null;
1853
+ };
1854
+ /** How long to wait between polls of a video generation job. */
1855
+ const VIDEO_POLL_INTERVAL_MS = 10_000;
1856
+ /** How long to keep polling before giving up on a video generation job. */
1857
+ const VIDEO_POLL_TIMEOUT_MS = 600_000;
1858
+ /**
1859
+ * Sleeps for the given number of milliseconds.
1860
+ * @param ms - Milliseconds to wait
1861
+ */
1862
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1863
+ /**
1864
+ * Generates a video with Google Veo.
1865
+ *
1866
+ * Video generation is a long-running operation on both providers — a Veo clip
1867
+ * takes on the order of a minute or two, far past any request timeout — so the
1868
+ * SDK returns an operation handle and this polls it to completion.
1869
+ *
1870
+ * The finished clip comes back either as inline base64 (`videoBytes`) or as a
1871
+ * `uri` on Google's file service, depending on model and size. The uri is NOT
1872
+ * public: it needs the same API key as the generation call, sent as a header,
1873
+ * which is why this fetches it here rather than handing the caller a link that
1874
+ * would 403 the moment it left the process.
1875
+ *
1876
+ * @param modelName - A Veo model id, e.g. `"veo-3.1-fast-generate-preview"`
1877
+ * @param config - Configuration object; must include `googleAPIKey`
1878
+ * @param prompt - The text prompt describing the video
1879
+ * @param options - `aspectRatio`, `resolution`, `durationSeconds`,
1880
+ * `negativePrompt`, `personGeneration`, `pollTimeoutMs`
1881
+ * @param usageTracker - Accumulator billed per second of generated video
1882
+ * @returns `{ b64_json, mimeType, seconds, model }`
1883
+ */
1884
+ const generateVideoWithGoogle = async (modelName, config, prompt, options = {}, usageTracker = null) => {
1885
+ const { aspectRatio = "16:9", resolution = "720p", durationSeconds = 8, negativePrompt, personGeneration, pollTimeoutMs = VIDEO_POLL_TIMEOUT_MS, } = options;
1886
+ const ai = new GoogleGenAI({ apiKey: config.googleAPIKey });
1887
+ let operation = await ai.models.generateVideos({
1888
+ model: modelName,
1889
+ prompt,
1890
+ config: {
1891
+ aspectRatio,
1892
+ resolution,
1893
+ durationSeconds,
1894
+ numberOfVideos: 1,
1895
+ ...(negativePrompt ? { negativePrompt } : {}),
1896
+ ...(personGeneration ? { personGeneration } : {}),
1897
+ },
1898
+ });
1899
+ const deadline = Date.now() + pollTimeoutMs;
1900
+ while (!operation.done) {
1901
+ if (Date.now() > deadline) {
1902
+ throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `Video generation with "${modelName}" did not finish within ${Math.round(pollTimeoutMs / 1000)} seconds.`);
1903
+ }
1904
+ await delay(VIDEO_POLL_INTERVAL_MS);
1905
+ operation = await ai.operations.getVideosOperation({ operation });
1906
+ }
1907
+ if (operation.error) {
1908
+ throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `Video generation with "${modelName}" failed: ${operation.error?.message || JSON.stringify(operation.error)}`);
1909
+ }
1910
+ const video = operation.response?.generatedVideos?.[0]?.video;
1911
+ if (!video) {
1912
+ throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `No video was generated by ${modelName}`);
1913
+ }
1914
+ let b64 = video.videoBytes;
1915
+ if (!b64) {
1916
+ if (!video.uri) {
1917
+ throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `${modelName} returned a video with neither inline bytes nor a uri`);
1918
+ }
1919
+ const download = await fetch(video.uri, {
1920
+ headers: { "x-goog-api-key": config.googleAPIKey },
1921
+ });
1922
+ if (!download.ok) {
1923
+ throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `Downloading the generated video failed with ${download.status} ${download.statusText}`);
1924
+ }
1925
+ b64 = Buffer.from(await download.arrayBuffer()).toString("base64");
1926
+ }
1927
+ // Billed on the duration ASKED FOR: the provider bills the clip it rendered,
1928
+ // and there is no returned duration to read back.
1929
+ updateVideoUsageTracker(usageTracker, modelName, durationSeconds, config);
1930
+ return {
1931
+ b64_json: b64,
1932
+ mimeType: video.mimeType || "video/mp4",
1933
+ seconds: durationSeconds,
1934
+ model: modelName,
1935
+ };
1936
+ };
1937
+ /**
1938
+ * Generates a video with OpenAI Sora.
1939
+ *
1940
+ * Like Veo this is a job rather than a request: `videos.create` returns
1941
+ * immediately with a `queued` job, which is polled until it completes and then
1942
+ * downloaded as bytes.
1943
+ *
1944
+ * @param modelName - A Sora model id, e.g. `"sora-2"`
1945
+ * @param config - Configuration object; must include `openAIAPIKey`
1946
+ * @param prompt - The text prompt describing the video
1947
+ * @param options - `size` (`"1280x720"` etc.), `durationSeconds` (4, 8 or 12),
1948
+ * `pollTimeoutMs`
1949
+ * @param usageTracker - Accumulator billed per second of generated video
1950
+ * @returns `{ b64_json, mimeType, seconds, model }`
1951
+ */
1952
+ const generateVideoWithOpenAI = async (modelName, config, prompt, options = {}, usageTracker = null) => {
1953
+ const { size = "1280x720", durationSeconds = 8, pollTimeoutMs = VIDEO_POLL_TIMEOUT_MS, } = options;
1954
+ const openai = new OpenAI({ apiKey: config.openAIAPIKey });
1955
+ // Sora takes the duration as one of a fixed set of STRINGS ("4", "8", "12").
1956
+ // A number, or any other value, is rejected by the API.
1957
+ const seconds = String(durationSeconds);
1958
+ if (!["4", "8", "12"].includes(seconds)) {
1959
+ throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, `Invalid durationSeconds "${durationSeconds}" for ${modelName}. Sora supports 4, 8 or 12.`);
1960
+ }
1961
+ let job = await openai.videos.create({
1962
+ model: modelName,
1963
+ prompt,
1964
+ seconds: seconds,
1965
+ size: size,
1966
+ });
1967
+ const deadline = Date.now() + pollTimeoutMs;
1968
+ while (job.status === "queued" || job.status === "in_progress") {
1969
+ if (Date.now() > deadline) {
1970
+ throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `Video generation with "${modelName}" did not finish within ${Math.round(pollTimeoutMs / 1000)} seconds.`);
1971
+ }
1972
+ await delay(VIDEO_POLL_INTERVAL_MS);
1973
+ job = await openai.videos.retrieve(job.id);
1974
+ }
1975
+ if (job.status !== "completed") {
1976
+ throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `Video generation with "${modelName}" ${job.status}: ${job.error?.message || "no reason given"}`);
1977
+ }
1978
+ const content = await openai.videos.downloadContent(job.id);
1979
+ const b64 = Buffer.from(await content.arrayBuffer()).toString("base64");
1980
+ updateVideoUsageTracker(usageTracker, modelName, Number(job.seconds) || Number(seconds), config);
1981
+ return {
1982
+ b64_json: b64,
1983
+ mimeType: "video/mp4",
1984
+ seconds: Number(job.seconds) || Number(seconds),
1985
+ model: modelName,
1986
+ };
1987
+ };
1988
+ /**
1989
+ * Top-level entry point for AI video generation, mirroring
1990
+ * `runPromptWithModelForImageGeneration`. Routes to Veo or Sora on the model
1991
+ * name prefix and logs the total generation time on success.
1992
+ *
1993
+ * Unlike image generation this always returns bytes — never a url. Both
1994
+ * providers hand back a link that requires their API key, so a url would be
1995
+ * useless to any caller that is not this process.
1996
+ *
1997
+ * @param modelName - The video model identifier; must start with `veo-` or `sora-`
1998
+ * @param config - Configuration object with `googleAPIKey` or `openAIAPIKey`
1999
+ * @param prompt - The text prompt describing the video to generate
2000
+ * @param options - Provider-specific generation options
2001
+ * @param usageTracker - Accumulator billed per second of generated video
2002
+ * @returns `{ b64_json, mimeType, seconds, model }`
2003
+ */
2004
+ const runPromptWithModelForVideoGeneration = async (modelName, config, prompt, options = {}, usageTracker = null) => {
2005
+ const provider = getVideoModelProvider(modelName);
2006
+ if (!provider) {
2007
+ throw errorHandlingHelper.create(constants.HTTP_CODES.BAD_REQUEST, `Unable to determine provider for model: ${modelName}. Model name should start with 'veo-' or 'sora-'.`);
2008
+ }
2009
+ const startTime = Date.now();
2010
+ try {
2011
+ const result = provider === "openai"
2012
+ ? await generateVideoWithOpenAI(modelName, config, prompt, options, usageTracker)
2013
+ : await generateVideoWithGoogle(modelName, config, prompt, options, usageTracker);
2014
+ const duration = Date.now() - startTime;
2015
+ logger.log(null, logger.levels.info, `Video generation with "${modelName}" completed in ${Math.round(duration / 1000)} seconds.`);
2016
+ return result;
2017
+ }
2018
+ catch (error) {
2019
+ if (error?.code && error?.message) {
2020
+ throw error;
2021
+ }
2022
+ if (error?.status || error?.code) {
2023
+ const statusCode = error.status || constants.HTTP_CODES.INTERNAL_SERVER_ERROR;
2024
+ throw errorHandlingHelper.create(statusCode, error.message || "Video generation failed");
2025
+ }
2026
+ throw errorHandlingHelper.create(constants.HTTP_CODES.INTERNAL_SERVER_ERROR, `Video generation failed: ${error?.message || String(error)}`);
2027
+ }
2028
+ };
1767
2029
  export default {
1768
2030
  checkIfAIProviderConfigured,
1769
2031
  createAgent,
1770
2032
  runAgent,
1771
2033
  runPromptWithModel,
1772
2034
  runPromptWithModelForImageGeneration,
2035
+ runPromptWithModelForVideoGeneration,
1773
2036
  throwErrorIfNotSuccessful,
1774
2037
  updateUsageTrackerForCharacters,
1775
2038
  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.1",
7
7
  "description": "",
8
8
  "main": "dist/cjs/index.js",
9
9
  "module": "dist/esm/index.js",