openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
234 lines (233 loc) • 9.22 kB
JavaScript
import { l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js";
import { o as createProviderHttpError } from "./provider-http-errors-DqaqQLLZ.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import "./provider-http-BXCBCi4E.js";
import { r as sanitizeAndNormalizeEmbedding } from "./embeddings-C9SBNXgr.js";
import { C as withRemoteHttpResponse, _ as buildBatchHeaders, g as runEmbeddingBatchGroups, h as buildEmbeddingBatchGroupOptions, o as debugEmbeddingsLog, v as normalizeBatchBaseUrl } from "./memory-core-host-engine-embeddings-nTgmnTkX.js";
import crypto from "node:crypto";
//#region extensions/google/embedding-batch.ts
const GEMINI_BATCH_MAX_REQUESTS = 5e4;
function hashText(text) {
return crypto.createHash("sha256").update(text).digest("hex");
}
function getGeminiUploadUrl(baseUrl) {
if (baseUrl.includes("/v1beta")) return baseUrl.replace(/\/v1beta\/?$/, "/upload/v1beta");
return `${baseUrl.replace(/\/$/, "")}/upload`;
}
function buildGeminiUploadBody(params) {
const boundary = `openclaw-${hashText(params.displayName)}`;
const jsonPart = JSON.stringify({ file: {
displayName: params.displayName,
mimeType: "application/jsonl"
} });
const delimiter = `--${boundary}\r\n`;
const closeDelimiter = `--${boundary}--\r\n`;
const parts = [
`${delimiter}Content-Type: application/json; charset=UTF-8\r\n\r\n${jsonPart}\r\n`,
`${delimiter}Content-Type: application/jsonl; charset=UTF-8\r\n\r\n${params.jsonl}\r\n`,
closeDelimiter
];
return {
body: new Blob([parts.join("")], { type: "multipart/related" }),
contentType: `multipart/related; boundary=${boundary}`
};
}
async function submitGeminiBatch(params) {
const baseUrl = normalizeBatchBaseUrl(params.gemini);
const uploadPayload = buildGeminiUploadBody({
jsonl: params.requests.map((request) => JSON.stringify({
key: request.custom_id,
request: request.request
})).join("\n"),
displayName: `memory-embeddings-${hashText(String(Date.now()))}`
});
const uploadUrl = `${getGeminiUploadUrl(baseUrl)}/files?uploadType=multipart`;
debugEmbeddingsLog("memory embeddings: gemini batch upload", {
uploadUrl,
baseUrl,
requests: params.requests.length
});
const filePayload = await withRemoteHttpResponse({
url: uploadUrl,
ssrfPolicy: params.gemini.ssrfPolicy,
init: {
method: "POST",
headers: {
...buildBatchHeaders(params.gemini, { json: false }),
"Content-Type": uploadPayload.contentType
},
body: uploadPayload.body
},
onResponse: async (fileRes) => {
if (!fileRes.ok) {
const text = await fileRes.text();
throw new Error(`gemini batch file upload failed: ${fileRes.status} ${text}`);
}
return await fileRes.json();
}
});
const fileId = filePayload.name ?? filePayload.file?.name;
if (!fileId) throw new Error("gemini batch file upload failed: missing file id");
const batchBody = { batch: {
displayName: `memory-embeddings-${params.agentId}`,
inputConfig: { file_name: fileId }
} };
const batchEndpoint = `${baseUrl}/${params.gemini.modelPath}:asyncBatchEmbedContent`;
debugEmbeddingsLog("memory embeddings: gemini batch create", {
batchEndpoint,
fileId
});
return await withRemoteHttpResponse({
url: batchEndpoint,
ssrfPolicy: params.gemini.ssrfPolicy,
init: {
method: "POST",
headers: buildBatchHeaders(params.gemini, { json: true }),
body: JSON.stringify(batchBody)
},
onResponse: async (batchRes) => {
if (batchRes.ok) return await batchRes.json();
const text = await batchRes.text();
if (batchRes.status === 404) throw new Error("gemini batch create failed: 404 (asyncBatchEmbedContent not available for this model/baseUrl). Disable remote.batch.enabled or switch providers.");
throw new Error(`gemini batch create failed: ${batchRes.status} ${text}`);
}
});
}
async function fetchGeminiBatchStatus(params) {
const statusUrl = `${normalizeBatchBaseUrl(params.gemini)}/${params.batchName.startsWith("batches/") ? params.batchName : `batches/${params.batchName}`}`;
debugEmbeddingsLog("memory embeddings: gemini batch status", { statusUrl });
return await withRemoteHttpResponse({
url: statusUrl,
ssrfPolicy: params.gemini.ssrfPolicy,
init: { headers: buildBatchHeaders(params.gemini, { json: true }) },
onResponse: async (res) => {
if (!res.ok) throw await createProviderHttpError(res, "gemini batch status failed");
return await res.json();
}
});
}
async function fetchGeminiFileContent(params) {
const downloadUrl = `${normalizeBatchBaseUrl(params.gemini)}/${params.fileId.startsWith("files/") ? params.fileId : `files/${params.fileId}`}:download`;
debugEmbeddingsLog("memory embeddings: gemini batch download", { downloadUrl });
return await withRemoteHttpResponse({
url: downloadUrl,
ssrfPolicy: params.gemini.ssrfPolicy,
init: { headers: buildBatchHeaders(params.gemini, { json: true }) },
onResponse: async (res) => {
if (!res.ok) throw await createProviderHttpError(res, "gemini batch file content failed");
return await res.text();
}
});
}
function parseGeminiBatchOutput(text) {
if (!text.trim()) return [];
return normalizeStringEntries(text.split("\n")).map((line) => JSON.parse(line));
}
async function waitForGeminiBatch(params) {
const start = Date.now();
let current = params.initial;
while (true) {
const status = current ?? await fetchGeminiBatchStatus({
gemini: params.gemini,
batchName: params.batchName
});
const state = status.state ?? "UNKNOWN";
if ([
"SUCCEEDED",
"COMPLETED",
"DONE"
].includes(state)) {
const outputFileId = status.outputConfig?.file ?? status.outputConfig?.fileId ?? status.metadata?.output?.responsesFile;
if (!outputFileId) throw new Error(`gemini batch ${params.batchName} completed without output file`);
return { outputFileId };
}
if ([
"FAILED",
"CANCELLED",
"CANCELED",
"EXPIRED"
].includes(state)) {
const message = status.error?.message ?? "unknown error";
throw new Error(`gemini batch ${params.batchName} ${state}: ${message}`);
}
if (!params.wait) throw new Error(`gemini batch ${params.batchName} still ${state}; wait disabled`);
if (Date.now() - start > params.timeoutMs) throw new Error(`gemini batch ${params.batchName} timed out after ${params.timeoutMs}ms`);
params.debug?.(`gemini batch ${params.batchName} ${state}; waiting ${params.pollIntervalMs}ms`);
await new Promise((resolve) => {
setTimeout(resolve, params.pollIntervalMs);
});
current = void 0;
}
}
async function runGeminiEmbeddingBatches(params) {
return await runEmbeddingBatchGroups({
...buildEmbeddingBatchGroupOptions(params, {
maxRequests: GEMINI_BATCH_MAX_REQUESTS,
debugLabel: "memory embeddings: gemini batch submit"
}),
runGroup: async ({ group, groupIndex, groups, byCustomId, pollIntervalMs, timeoutMs }) => {
const batchInfo = await submitGeminiBatch({
gemini: params.gemini,
requests: group,
agentId: params.agentId
});
const batchName = batchInfo.name ?? "";
if (!batchName) throw new Error("gemini batch create failed: missing batch name");
params.debug?.("memory embeddings: gemini batch created", {
batchName,
state: batchInfo.state,
group: groupIndex + 1,
groups,
requests: group.length
});
if (!params.wait && batchInfo.state && ![
"SUCCEEDED",
"COMPLETED",
"DONE"
].includes(batchInfo.state)) throw new Error(`gemini batch ${batchName} submitted; enable remote.batch.wait to await completion`);
const completed = batchInfo.state && [
"SUCCEEDED",
"COMPLETED",
"DONE"
].includes(batchInfo.state) ? { outputFileId: batchInfo.outputConfig?.file ?? batchInfo.outputConfig?.fileId ?? batchInfo.metadata?.output?.responsesFile ?? "" } : await waitForGeminiBatch({
gemini: params.gemini,
batchName,
wait: params.wait,
pollIntervalMs,
timeoutMs,
debug: params.debug,
initial: batchInfo
});
if (!completed.outputFileId) throw new Error(`gemini batch ${batchName} completed without output file`);
const outputLines = parseGeminiBatchOutput(await fetchGeminiFileContent({
gemini: params.gemini,
fileId: completed.outputFileId
}));
const errors = [];
const remaining = new Set(group.map((request) => request.custom_id));
for (const line of outputLines) {
const customId = line.key ?? line.custom_id ?? line.request_id;
if (!customId) continue;
remaining.delete(customId);
if (line.error?.message) {
errors.push(`${customId}: ${line.error.message}`);
continue;
}
if (line.response?.error?.message) {
errors.push(`${customId}: ${line.response.error.message}`);
continue;
}
const embedding = sanitizeAndNormalizeEmbedding(line.embedding?.values ?? line.response?.embedding?.values ?? []);
if (embedding.length === 0) {
errors.push(`${customId}: empty embedding`);
continue;
}
byCustomId.set(customId, embedding);
}
if (errors.length > 0) throw new Error(`gemini batch ${batchName} failed: ${errors.join("; ")}`);
if (remaining.size > 0) throw new Error(`gemini batch ${batchName} missing ${remaining.size} embedding responses`);
}
});
}
//#endregion
export { runGeminiEmbeddingBatches as t };