@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.
package/dist/cjs/langChain.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/cjs/langChain.js
CHANGED
|
@@ -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,
|
package/dist/esm/langChain.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/esm/langChain.js
CHANGED
|
@@ -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,
|