@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.
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),
|
|
@@ -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,
|
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),
|
|
@@ -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,
|