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