UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

303 lines (302 loc) • 13.7 kB
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs"; import { a as asOptionalRecord } from "./record-coerce-TIsquoVR.mjs"; import { o as createProviderHttpError, p as readProviderJsonObjectResponse, y as readProviderResponseErrorText } from "./provider-http-errors-DmewLLH7.mjs"; import { D as buildRemoteBaseUrlPolicy, O as withRemoteHttpResponse, j as sanitizeAndNormalizeEmbedding } from "./gateway-startup-plugin-config-DOkflSjl.mjs"; import { A as requireApiKey } from "./model-auth-provider-config-C6nCoXKi.mjs"; import "./string-coerce-runtime-50qKeWeL.mjs"; import { r as providerOperationRetryConfig } from "./operation-retry-Ci68Y-og.mjs"; import { n as executeWithApiKeyRotation, t as collectProviderApiKeysForExecution } from "./api-key-rotation-DVJfHOV_.mjs"; import { a as resolveApiKeyForProvider } from "./provider-auth-runtime-JvyP44zh.mjs"; import "./provider-http-CuRGJD0L.mjs"; import { a as normalizeEmbeddingModelWithPrefixes, c as embeddingProviderOwnsDestination, l as resolveEmbeddingEndpointUrl, o as debugEmbeddingsLog } from "./memory-core-host-engine-embeddings-D4RTPmUb.mjs"; import { n as resolveMemorySecretInputString } from "./secret-input-Bfq9Btd3.mjs"; import "./memory-core-host-secret-gvjKiv8R.mjs"; import { t as parseGeminiAuth } from "./gemini-auth-ms1_pxuH.mjs"; import { t as resolveGoogleApiClientHeaders } from "./google-api-client-header-Dfv7yW-z.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(embedding) { const value = asOptionalRecord(embedding)?.values; if (!isValidGeminiEmbeddingValues(value)) throw malformedGeminiEmbeddingResponse(); return value; } function readGeminiBatchEmbeddings(payload, expectedCount) { if (!Array.isArray(payload.embeddings) || payload.embeddings.length !== expectedCount) throw malformedGeminiEmbeddingResponse(); return payload.embeddings.map(readGeminiEmbeddingValues); } 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 normalizeGeminiModel(model) { return normalizeEmbeddingModelWithPrefixes({ model, defaultModel: DEFAULT_GEMINI_EMBEDDING_MODEL, prefixes: [ "models/gemini/", "models/google/", "models/", "gemini/", "google/" ] }); } /** * 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; const url = URL.parse(trimmed); if (!url) return 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(/\/$/, ""); } 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(readGeminiEmbeddingValues((await fetchGeminiEmbeddingPayload({ client, endpoint: embedUrl, body: buildGeminiEmbeddingRequest({ input: text, model: client.model, role: "query", taskType: options.taskType ?? "RETRIEVAL_QUERY", outputDimensionality }), signal: callOptions?.signal })).embedding), 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 = resolveMemorySecretInputString({ value: remote?.apiKey, path: "memory.search.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 = model.startsWith("models/") ? model : `models/${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 };