openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
315 lines (314 loc) • 14.3 kB
JavaScript
import { l as normalizeOptionalString } from "./string-coerce-6kR-86XC.mjs";
import { a as asOptionalRecord } from "./record-coerce-TIsquoVR.mjs";
import { o as createProviderHttpError, p as readProviderJsonObjectResponse, y as readProviderResponseErrorText } from "./provider-http-errors-xcK_t3aM.mjs";
import { M as sanitizeAndNormalizeEmbedding, O as buildRemoteBaseUrlPolicy, k as withRemoteHttpResponse } from "./gateway-startup-plugin-config-J-HqkJwK.mjs";
import { j as requireApiKey } from "./model-auth-provider-config-D93sNwG4.mjs";
import "./string-coerce-runtime-B1b8UNzb.mjs";
import { r as providerOperationRetryConfig } from "./operation-retry-BnETYXi4.mjs";
import { n as executeWithApiKeyRotation, t as collectProviderApiKeysForExecution } from "./api-key-rotation-DkRUZHoX.mjs";
import { a as resolveApiKeyForProvider } from "./provider-auth-runtime-CdsNdEp6.mjs";
import "./provider-http-fLggMdVs.mjs";
import { c as embeddingProviderOwnsDestination, l as resolveEmbeddingEndpointUrl, o as debugEmbeddingsLog } from "./memory-core-host-engine-embeddings-Dh7SoebN.mjs";
import { n as resolveMemorySecretInputString } from "./secret-input-D44Anmkw.mjs";
import "./memory-core-host-secret-BGcd8nWK.mjs";
import { t as parseGeminiAuth } from "./gemini-auth-DXoq9U0n.mjs";
import { t as resolveGoogleApiClientHeaders } from "./google-api-client-header-DqEViYoI.mjs";
//#region extensions/google/embedding-provider.ts
const DEFAULT_GEMINI_EMBEDDING_MODEL = "gemini-embedding-001";
const DEFAULT_GOOGLE_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
const GEMINI_MAX_INPUT_TOKENS = {
"gemini-embedding-001": 2048,
"gemini-embedding-2": 8192,
"gemini-embedding-2-preview": 8192
};
const GEMINI_EMBEDDING_2_MODELS = /* @__PURE__ */ new Set(["gemini-embedding-2", "gemini-embedding-2-preview"]);
const GEMINI_EMBEDDING_2_DEFAULT_DIMENSIONS = 3072;
const GOOGLE_RETRY_INFO_TYPE = "type.googleapis.com/google.rpc.RetryInfo";
const GOOGLE_RETRY_DELAY_RE = /^(\d+)(?:\.(\d{1,9}))?s$/u;
const GOOGLE_RETRY_INFO_BODY_LIMIT_BYTES = 16384;
const GEMINI_EMBEDDING_2_TASK_PREFIXES = {
RETRIEVAL_QUERY: "task: search result | query:",
RETRIEVAL_DOCUMENT: "title: none | text:",
SEMANTIC_SIMILARITY: "task: sentence similarity | query:",
CLASSIFICATION: "task: classification | query:",
CLUSTERING: "task: clustering | query:",
QUESTION_ANSWERING: "task: question answering | query:",
FACT_VERIFICATION: "task: fact checking | query:"
};
function malformedGeminiEmbeddingResponse() {
return /* @__PURE__ */ new Error("gemini embeddings failed: malformed JSON response");
}
function unexpectedGeminiEmbeddingDimensions(expected, actual) {
return /* @__PURE__ */ new Error(`gemini embeddings failed: expected ${expected} dimensions, received ${actual}`);
}
/** Direct and downloaded vectors share one invariant before normalization. */
function isValidGeminiEmbeddingValues(value) {
return Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === "number" && Number.isFinite(entry));
}
function readGeminiEmbeddingValues(value) {
if (!isValidGeminiEmbeddingValues(value)) throw malformedGeminiEmbeddingResponse();
return value;
}
function readGeminiSingleEmbedding(payload) {
const embedding = asOptionalRecord(payload.embedding);
if (!embedding) throw malformedGeminiEmbeddingResponse();
return readGeminiEmbeddingValues(embedding.values);
}
function readGeminiBatchEmbeddings(payload, expectedCount) {
if (!Array.isArray(payload.embeddings) || payload.embeddings.length !== expectedCount) throw malformedGeminiEmbeddingResponse();
return payload.embeddings.map((entry) => {
const embedding = asOptionalRecord(entry);
if (!embedding) throw malformedGeminiEmbeddingResponse();
return readGeminiEmbeddingValues(embedding.values);
});
}
function buildGeminiEmbeddingRequest(params) {
const input = typeof params.input === "string" ? { text: params.input } : params.input;
const parts = input.parts?.map((part) => part.type === "text" ? { text: part.text } : { inlineData: {
mimeType: part.mimeType,
data: part.data
} }) ?? [{ text: input.text }];
const isStableEmbedding2 = normalizeGeminiModel(params.model) === "gemini-embedding-2";
const request = { content: { parts } };
if (isStableEmbedding2 && parts.every((part) => "text" in part)) {
const first = parts[0];
if (first && "text" in first) {
const taskType = params.role === "document" && (params.taskType === "RETRIEVAL_QUERY" || params.taskType === "QUESTION_ANSWERING" || params.taskType === "FACT_VERIFICATION") ? "RETRIEVAL_DOCUMENT" : params.taskType;
first.text = `${GEMINI_EMBEDDING_2_TASK_PREFIXES[taskType]} ${first.text}`;
}
} else if (!isStableEmbedding2) request.taskType = params.taskType;
if (params.modelPath) request.model = params.modelPath;
if (params.outputDimensionality != null) request.outputDimensionality = params.outputDimensionality;
return request;
}
/** Returns true for Gemini Embedding 2 variants with multimodal and extended task support. */
function isGeminiEmbedding2Model(model) {
return GEMINI_EMBEDDING_2_MODELS.has(normalizeGeminiModel(model));
}
function resolveGeminiOutputDimensionality(model, requested) {
const isEmbedding2 = isGeminiEmbedding2Model(model);
if (!isEmbedding2 && model !== "gemini-embedding-001") return;
if (requested == null) return isEmbedding2 ? GEMINI_EMBEDDING_2_DEFAULT_DIMENSIONS : void 0;
if (!Number.isInteger(requested) || requested < 128 || requested > 3072) throw new Error(`Invalid outputDimensionality ${requested} for ${model}. Use an integer between 128 and 3072.`);
return requested;
}
function resolveRemoteApiKey(remoteApiKey) {
return resolveMemorySecretInputString({
value: remoteApiKey,
path: "memory.search.remote.apiKey"
});
}
function normalizeGeminiModel(model) {
const trimmed = model.trim();
if (!trimmed) return DEFAULT_GEMINI_EMBEDDING_MODEL;
const withoutPrefix = trimmed.replace(/^models\//, "");
if (withoutPrefix.startsWith("gemini/")) return withoutPrefix.slice(7);
if (withoutPrefix.startsWith("google/")) return withoutPrefix.slice(7);
return withoutPrefix;
}
/**
* Parses the delay hint from a `google.rpc.RetryInfo` detail inside a Gemini error
* body. Callers should prefer a full (bounded) body read over the 500-char
* `ProviderHttpError.errorBody` preview: real Gemini 429 payloads run ~1KB+ with
* the RetryInfo detail near the end of `error.details`, so the truncated preview
* regularly cuts the hint away.
*/
function extractGoogleRetryInfoDelayMs(errorBody) {
if (typeof errorBody !== "string" || !errorBody) return;
try {
const details = asOptionalRecord(asOptionalRecord(JSON.parse(errorBody))?.error)?.details;
if (!Array.isArray(details)) return;
let retryAfterMs;
for (const rawDetail of details) {
const detail = asOptionalRecord(rawDetail);
const retryDelay = normalizeOptionalString(detail?.retryDelay);
const match = retryDelay ? GOOGLE_RETRY_DELAY_RE.exec(retryDelay) : void 0;
if (normalizeOptionalString(detail?.["@type"]) !== GOOGLE_RETRY_INFO_TYPE || !match) continue;
const delayMs = Number(match[1]) * 1e3 + (match[2] ? Math.ceil(Number(`0.${match[2]}`) * 1e3) : 0);
if (Number.isSafeInteger(delayMs) && delayMs >= 0) retryAfterMs = Math.max(retryAfterMs ?? delayMs, delayMs);
}
return retryAfterMs;
} catch {
return;
}
}
function sanitizeGeminiEmbedding(values, expectedDimensions) {
if (expectedDimensions != null && values.length !== expectedDimensions) throw unexpectedGeminiEmbeddingDimensions(expectedDimensions, values.length);
return sanitizeAndNormalizeEmbedding(values);
}
async function fetchGeminiEmbeddingPayload(params) {
return await executeWithApiKeyRotation({
provider: "google",
apiKeys: params.client.apiKeys,
transientRetry: providerOperationRetryConfig("read"),
execute: async (apiKey) => {
const headers = {
...parseGeminiAuth(apiKey).headers,
...params.client.headers
};
return await withRemoteHttpResponse({
url: params.endpoint,
ssrfPolicy: params.client.ssrfPolicy,
signal: params.signal,
init: {
method: "POST",
headers,
body: JSON.stringify(params.body)
},
onResponse: async (res) => {
if (!res.ok) {
let errorBodyClone;
try {
errorBodyClone = res.clone();
} catch {
errorBodyClone = void 0;
}
try {
const error = await createProviderHttpError(res, "gemini embeddings failed", {
requestHeaders: headers,
signal: params.signal
});
let retryInfoSource = error.errorBody;
if (errorBodyClone) try {
retryInfoSource = await readProviderResponseErrorText(errorBodyClone, GOOGLE_RETRY_INFO_BODY_LIMIT_BYTES, headers, params.signal);
} catch {
params.signal?.throwIfAborted();
retryInfoSource = error.errorBody;
}
params.signal?.throwIfAborted();
const retryAfterMs = extractGoogleRetryInfoDelayMs(retryInfoSource);
if (retryAfterMs !== void 0) error.retryAfterMs = Math.max(retryAfterMs, error.retryAfterMs ?? 0);
throw error;
} finally {
errorBodyClone?.body?.cancel().catch(() => void 0);
}
}
return await readProviderJsonObjectResponse(res, "gemini embeddings failed");
}
});
}
});
}
function normalizeGeminiBaseUrl(raw) {
const trimmed = raw.trim();
if (!trimmed) return DEFAULT_GOOGLE_API_BASE_URL;
try {
const url = new URL(trimmed);
url.hash = "";
const openAiIndex = url.pathname.indexOf("/openai");
url.pathname = (openAiIndex < 0 ? url.pathname : url.pathname.slice(0, openAiIndex)).replace(/\/+$/, "");
if (url.origin.toLowerCase() === "https://generativelanguage.googleapis.com" && url.pathname === "/") url.pathname = "/v1beta";
return url.search ? url.href : url.href.replace(/\/$/, "");
} catch {
return trimmed;
}
}
function buildGeminiModelPath(model) {
return model.startsWith("models/") ? model : `models/${model}`;
}
async function createGeminiEmbeddingProvider(options) {
const client = await resolveGeminiEmbeddingClient(options);
const embedUrl = resolveEmbeddingEndpointUrl(client.baseUrl, `${client.modelPath}:embedContent`);
const batchUrl = resolveEmbeddingEndpointUrl(client.baseUrl, `${client.modelPath}:batchEmbedContents`);
const outputDimensionality = client.outputDimensionality;
const embedQuery = async (text, callOptions) => {
if (!text.trim()) return [];
return sanitizeGeminiEmbedding(readGeminiSingleEmbedding(await fetchGeminiEmbeddingPayload({
client,
endpoint: embedUrl,
body: buildGeminiEmbeddingRequest({
input: text,
model: client.model,
role: "query",
taskType: options.taskType ?? "RETRIEVAL_QUERY",
outputDimensionality
}),
signal: callOptions?.signal
})), outputDimensionality);
};
const embedDocuments = async (inputs, callOptions) => {
if (inputs.length === 0) return [];
return readGeminiBatchEmbeddings(await fetchGeminiEmbeddingPayload({
client,
endpoint: batchUrl,
body: { requests: inputs.map((input) => buildGeminiEmbeddingRequest({
input,
model: client.model,
role: "document",
modelPath: client.modelPath,
taskType: options.taskType ?? "RETRIEVAL_DOCUMENT",
outputDimensionality
})) },
signal: callOptions?.signal
}), inputs.length).map((values) => sanitizeGeminiEmbedding(values, outputDimensionality));
};
return {
provider: {
id: "gemini",
model: client.model,
maxInputTokens: GEMINI_MAX_INPUT_TOKENS[client.model],
embed: async (input, callOptions) => {
if (callOptions?.inputType === "query") return await embedQuery(typeof input === "string" ? input : input.text, callOptions);
return (await embedDocuments([input], callOptions))[0] ?? [];
},
embedBatch: async (inputs, callOptions) => callOptions?.inputType === "query" ? await Promise.all(inputs.map((input) => embedQuery(typeof input === "string" ? input : input.text, callOptions))) : await embedDocuments(inputs, callOptions)
},
client
};
}
async function resolveGeminiEmbeddingClient(options) {
const remote = options.remote;
const remoteApiKey = resolveRemoteApiKey(remote?.apiKey);
const remoteBaseUrl = remote?.baseUrl?.trim();
const providerConfig = options.config.models?.providers?.google;
const providerBaseUrl = normalizeGeminiBaseUrl(normalizeOptionalString(providerConfig?.baseUrl) || DEFAULT_GOOGLE_API_BASE_URL);
const rawBaseUrl = remoteBaseUrl || providerBaseUrl;
const baseUrl = normalizeGeminiBaseUrl(rawBaseUrl);
const providerOwnsDestination = embeddingProviderOwnsDestination({
baseUrl,
providerBaseUrl
});
const apiKey = remoteApiKey ? remoteApiKey : providerOwnsDestination ? requireApiKey(await resolveApiKeyForProvider({
provider: "google",
cfg: options.config,
agentDir: options.agentDir
}), "google") : void 0;
if (!apiKey) throw new Error(`Google embedding credentials are not configured for ${baseUrl}. Set memory.search.remote.apiKey for this destination.`);
const ssrfPolicy = buildRemoteBaseUrlPolicy(baseUrl);
const headers = {
...Object.assign({}, providerOwnsDestination ? providerConfig?.headers : void 0, remote?.headers),
...resolveGoogleApiClientHeaders({
baseUrl,
api: "google-generative-ai",
capability: "other",
transport: "http"
})
};
const apiKeys = remoteApiKey ? [apiKey] : collectProviderApiKeysForExecution({
provider: "google",
primaryApiKey: apiKey
});
const model = normalizeGeminiModel(options.model);
const modelPath = buildGeminiModelPath(model);
const outputDimensionality = resolveGeminiOutputDimensionality(model, options.dimensions);
debugEmbeddingsLog("memory embeddings: gemini client", {
rawBaseUrl,
baseUrl,
model,
modelPath,
outputDimensionality,
embedEndpoint: resolveEmbeddingEndpointUrl(baseUrl, `${modelPath}:embedContent`),
batchEndpoint: resolveEmbeddingEndpointUrl(baseUrl, `${modelPath}:batchEmbedContents`)
});
return {
baseUrl,
headers,
ssrfPolicy,
model,
modelPath,
apiKeys,
outputDimensionality
};
}
//#endregion
export { isValidGeminiEmbeddingValues as a, isGeminiEmbedding2Model as i, buildGeminiEmbeddingRequest as n, sanitizeGeminiEmbedding as o, createGeminiEmbeddingProvider as r, DEFAULT_GEMINI_EMBEDDING_MODEL as t };