UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

308 lines (307 loc) 12.3 kB
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js"; import { Et as array, Rn as string, Tn as object, Xn as union, wn as number, yt as _enum } from "./schemas-zxit8y5H.js"; import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js"; import { u as resolveAgentDir } from "./agent-scope-config-DcbEhP0R.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import "./agent-scope-DbtJyKUL.js"; import "./io-bdCzpGWJ.js"; import { i as logWarn } from "./logger-DwECwNVZ.js"; import { n as createConfiguredProviderLocalServiceAcquirer } from "./provider-local-service-D9Gl9HJT.js"; import { t as resolveMemorySearchConfig } from "./memory-search-DTACVvxE.js"; import { t as getMemoryEmbeddingProvider } from "./memory-embedding-provider-runtime-6fQgM2Qk.js"; import { d as getHeader, h as resolveOpenAiCompatibleHttpOperatorScopes, o as authorizeOpenAiCompatibleHttpModelOverride } from "./http-auth-utils-C3lb4QXY.js"; import { l as sendMissingScopeForbidden, m as watchClientDisconnect, o as sendInvalidRequest, r as parseGatewayJsonRequest, s as sendJson } from "./http-common-BaZaosnr.js"; import { c as isUnknownGatewayAgentError, i as isAgentSelectionRequiredError, l as resolveAgentIdForRequest, s as isOpenClawAgentModelId } from "./http-utils-BHgXp7Zb.js"; import { t as handleGatewayPostJsonEndpoint } from "./http-endpoint-helpers-B6JLQadu.js"; import { Buffer } from "node:buffer"; //#region src/gateway/embeddings-http.ts const EmbeddingsRequestSchema = object({ model: string().optional(), input: union([string(), array(string())]).optional(), encoding_format: _enum(["float", "base64"]).optional(), dimensions: number().int().positive().optional(), user: string().optional() }); const DEFAULT_EMBEDDINGS_BODY_BYTES = 5242880; const MAX_EMBEDDING_INPUTS = 128; const MAX_EMBEDDING_INPUT_CHARS = 8192; const MAX_EMBEDDING_TOTAL_CHARS = 65536; const DEFAULT_MEMORY_EMBEDDING_PROVIDER = "openai"; const EMBEDDING_PROVIDER_RETIREMENTS = /* @__PURE__ */ new Map(); const EMBEDDING_PROVIDER_ADMISSION_TAILS = /* @__PURE__ */ new Map(); async function acquireEmbeddingProviderLease(scopeKey, signal, create, holdForCleanup) { const previous = EMBEDDING_PROVIDER_ADMISSION_TAILS.get(scopeKey) ?? Promise.resolve(); const createLease = async () => { signal.throwIfAborted(); await drainEmbeddingProviderRetirements(scopeKey); signal.throwIfAborted(); const provider = await create(); if (signal.aborted) { await closeEmbeddingProvider(scopeKey, provider); signal.throwIfAborted(); } if (!holdForCleanup(provider)) return { provider, lifecycle: Promise.resolve(), release: () => {} }; let release = () => {}; return { provider, lifecycle: new Promise((resolve) => { release = resolve; }), release }; }; const acquired = previous.then(createLease, createLease); const tail = acquired.then(async ({ lifecycle }) => await lifecycle).then(() => void 0, () => void 0); EMBEDDING_PROVIDER_ADMISSION_TAILS.set(scopeKey, tail); tail.then(() => { if (EMBEDDING_PROVIDER_ADMISSION_TAILS.get(scopeKey) === tail) EMBEDDING_PROVIDER_ADMISSION_TAILS.delete(scopeKey); }); const { provider, release } = await acquired; return { provider, release }; } async function drainEmbeddingProviderRetirements(scopeKey) { const pending = EMBEDDING_PROVIDER_RETIREMENTS.get(scopeKey); if (!pending || pending.size === 0) return; let firstError; let closeFailed = false; for (const provider of pending) try { await provider.close?.(); pending.delete(provider); } catch (err) { if (!closeFailed) firstError = err; closeFailed = true; } if (pending.size === 0) EMBEDDING_PROVIDER_RETIREMENTS.delete(scopeKey); if (closeFailed) throw firstError; } function retainEmbeddingProviderForRetirement(scopeKey, provider) { const pending = EMBEDDING_PROVIDER_RETIREMENTS.get(scopeKey) ?? /* @__PURE__ */ new Set(); pending.add(provider); EMBEDDING_PROVIDER_RETIREMENTS.set(scopeKey, pending); } async function closeEmbeddingProvider(scopeKey, provider) { try { await provider.close?.(); } catch (closeErr) { retainEmbeddingProviderForRetirement(scopeKey, provider); logWarn(`openai-compat: failed to close embeddings provider: ${formatErrorMessage(closeErr)}`); } } async function drainRetainedOpenAiEmbeddingProviders() { const activeLifecycles = Array.from(EMBEDDING_PROVIDER_ADMISSION_TAILS.values()); if (activeLifecycles.length > 0) await Promise.allSettled(activeLifecycles); let firstError; let closeFailed = false; for (const scopeKey of Array.from(EMBEDDING_PROVIDER_RETIREMENTS.keys())) try { await drainEmbeddingProviderRetirements(scopeKey); } catch (err) { if (!closeFailed) firstError = err; closeFailed = true; } if (closeFailed) throw firstError; } function resolveInputTexts(input) { if (typeof input === "string") return [input]; if (!Array.isArray(input)) return null; if (input.every((entry) => typeof entry === "string")) return input; return null; } function encodeEmbeddingBase64(embedding) { const float32 = Float32Array.from(embedding); return Buffer.from(float32.buffer).toString("base64"); } function validateInputTexts(texts) { if (texts.length === 0 || texts.some((text) => text.length === 0)) return "`input` must contain at least one non-empty string."; if (texts.length > MAX_EMBEDDING_INPUTS) return `Too many inputs (max ${MAX_EMBEDDING_INPUTS}).`; let totalChars = 0; for (const text of texts) { if (text.length > MAX_EMBEDDING_INPUT_CHARS) return `Input too long (max ${MAX_EMBEDDING_INPUT_CHARS} chars).`; totalChars += text.length; if (totalChars > MAX_EMBEDDING_TOTAL_CHARS) return `Total input too large (max ${MAX_EMBEDDING_TOTAL_CHARS} chars).`; } } function resolveEmbeddingProviderRemoteConfig(remote) { return remote ? { baseUrl: remote.baseUrl, apiKey: remote.apiKey, headers: remote.headers } : void 0; } function isLocalEmbeddingProvider(params) { const providerId = params.provider === "auto" ? DEFAULT_MEMORY_EMBEDDING_PROVIDER : params.provider; return getMemoryEmbeddingProvider(providerId, params.cfg)?.transport === "local"; } async function createConfiguredEmbeddingProvider(params) { const acquireLocalService = createConfiguredProviderLocalServiceAcquirer(() => params.cfg); const providerId = params.provider === "auto" ? DEFAULT_MEMORY_EMBEDDING_PROVIDER : params.provider; const adapter = getMemoryEmbeddingProvider(providerId, params.cfg); if (!adapter) throw new Error(`Unknown memory embedding provider: ${providerId}`); const createOptions = { config: params.cfg, agentDir: params.agentDir, provider: providerId, model: params.model || adapter.defaultModel || "", local: params.memorySearch?.local, remote: resolveEmbeddingProviderRemoteConfig(params.memorySearch?.remote), inputType: params.memorySearch?.inputType, queryInputType: params.memorySearch?.queryInputType, documentInputType: params.memorySearch?.documentInputType, dimensions: params.dimensions, fallback: "none", acquireLocalService }; const { provider } = await adapter.create(createOptions); if (!provider) throw new Error(`Memory embedding provider ${providerId} is unavailable.`); return provider; } function resolveEmbeddingsTarget(params) { const configuredProvider = params.configuredProvider === "auto" ? DEFAULT_MEMORY_EMBEDDING_PROVIDER : params.configuredProvider; const raw = params.requestModel.trim(); const slash = raw.indexOf("/"); if (slash === -1) return { provider: configuredProvider, model: raw }; const provider = normalizeLowercaseStringOrEmpty(raw.slice(0, slash)); const model = raw.slice(slash + 1).trim(); if (!model) return { errorMessage: "Unsupported embedding model reference." }; if (provider !== configuredProvider) return { errorMessage: "This agent does not allow that embedding provider on `/v1/embeddings`." }; return { provider: configuredProvider, model }; } /** Handles OpenAI-compatible embeddings requests for the configured agent memory provider. */ async function handleOpenAiEmbeddingsHttpRequest(req, res, opts) { const handled = await handleGatewayPostJsonEndpoint(req, res, { pathname: "/v1/embeddings", requiredOperatorMethod: "chat.send", resolveOperatorScopes: resolveOpenAiCompatibleHttpOperatorScopes, auth: opts.auth, trustedProxies: opts.trustedProxies, allowRealIpFallback: opts.allowRealIpFallback, rateLimiter: opts.rateLimiter, maxBodyBytes: opts.maxBodyBytes ?? DEFAULT_EMBEDDINGS_BODY_BYTES }); if (handled === false) return false; if (!handled) return true; const modelOverrideAuth = authorizeOpenAiCompatibleHttpModelOverride(req, handled.requestAuth); if (!modelOverrideAuth.allowed) { sendMissingScopeForbidden(res, modelOverrideAuth.missingScope); return true; } const payload = parseGatewayJsonRequest(res, handled.body, EmbeddingsRequestSchema); if (!payload) return true; const requestModel = normalizeOptionalString(payload.model) ?? ""; if (!requestModel) { sendInvalidRequest(res, "Missing `model`."); return true; } const cfg = getRuntimeConfig(); if (!isOpenClawAgentModelId(requestModel)) { sendInvalidRequest(res, "Invalid `model`. Use `openclaw` or `openclaw/<agentId>`."); return true; } const texts = resolveInputTexts(payload.input); if (!texts) { sendInvalidRequest(res, "`input` must be a string or an array of strings."); return true; } const inputError = validateInputTexts(texts); if (inputError) { sendInvalidRequest(res, inputError); return true; } let agentId; try { agentId = resolveAgentIdForRequest({ req, model: requestModel }); } catch (err) { if (isAgentSelectionRequiredError(err) || isUnknownGatewayAgentError(err)) { sendInvalidRequest(res, err.message); return true; } throw err; } const agentDir = resolveAgentDir(cfg, agentId); const memorySearch = resolveMemorySearchConfig(cfg, agentId); const configuredProvider = memorySearch?.provider ?? "openai"; const target = resolveEmbeddingsTarget({ requestModel: normalizeOptionalString(getHeader(req, "x-openclaw-model")) || normalizeOptionalString(memorySearch?.model) || "", configuredProvider }); if ("errorMessage" in target) { sendInvalidRequest(res, target.errorMessage); return true; } const providerScopeKey = JSON.stringify([agentId, target.provider]); const requestedProviderNeedsCleanup = isLocalEmbeddingProvider({ cfg, provider: target.provider }); if (req.socket.destroyed || res.destroyed || res.socket?.destroyed) return true; const abortController = new AbortController(); const stopWatchingDisconnect = watchClientDisconnect(req, res, abortController); try { const { provider, release } = await acquireEmbeddingProviderLease(providerScopeKey, abortController.signal, async () => await createConfiguredEmbeddingProvider({ cfg, agentDir, provider: target.provider, model: target.model, dimensions: payload.dimensions ?? memorySearch?.outputDimensionality, memorySearch: memorySearch ?? void 0 }), (createdProvider) => requestedProviderNeedsCleanup || isLocalEmbeddingProvider({ cfg, provider: createdProvider.id })); try { const embeddings = await provider.embedBatch(texts, { signal: abortController.signal, inputType: "document" }); if (abortController.signal.aborted) return true; const encodingFormat = payload.encoding_format === "base64" ? "base64" : "float"; sendJson(res, 200, { object: "list", data: embeddings.map((embedding, index) => ({ object: "embedding", index, embedding: encodingFormat === "base64" ? encodeEmbeddingBase64(embedding) : embedding })), model: requestModel, usage: { prompt_tokens: 0, total_tokens: 0 } }); } finally { try { await closeEmbeddingProvider(providerScopeKey, provider); } finally { release(); } } } catch (err) { if (!abortController.signal.aborted) { logWarn(`openai-compat: embeddings request failed: ${formatErrorMessage(err)}`); sendJson(res, 500, { error: { message: "internal error", type: "api_error" } }); } } finally { stopWatchingDisconnect(); } return true; } //#endregion export { drainRetainedOpenAiEmbeddingProviders, handleOpenAiEmbeddingsHttpRequest };