UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

298 lines (297 loc) 12.9 kB
import { c as isRecord } from "./record-coerce-DItp3I4t.js"; import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { m as normalizeResolvedSecretInputString } from "./types.secrets-kC0nOetj.js"; import { _ as truncateErrorDetail, o as createProviderHttpError, p as readProviderJsonObjectResponse, u as formatProviderHttpErrorMessage } from "./provider-http-errors-U-nhuk_f.js"; import { d as readPositiveIntegerParam, h as readToolStringParam } from "./common-Bm6UTDDA.js"; import { o as wrapWebContent } from "./external-content-CpqslxXH.js"; import { S as writeCachedSearchPayload, _ as resolveSearchTimeoutSeconds, a as buildUnsupportedSearchFilterResponse, f as readCachedSearchPayload, h as resolveSearchCacheTtlMs, i as buildSearchCacheKey, m as readProviderEnvValue, p as readConfiguredSecretString, u as parseWebSearchTimeFilters, x as withTrustedWebSearchEndpoint } from "./web-search-provider-common-CJ8tUQsS.js"; import "./string-coerce-runtime-GQa0ehRA.js"; import "./provider-http-k9RMI7iG.js"; import { n as resolveCitationRedirectUrl } from "./provider-web-search-Buu1PWOm.js"; import "./secret-input-runtime-Clavxxg-.js"; import { f as normalizeGoogleApiBaseUrl } from "./provider-policy-BjaZN4xs.js"; import { t as resolveGoogleApiClientHeaders } from "./google-api-client-header-DHDLnetg.js"; import { createHash } from "node:crypto"; //#region extensions/google/src/gemini-web-search-provider.shared.ts const DEFAULT_GEMINI_WEB_SEARCH_MODEL = "gemini-2.5-flash"; function resolveGeminiConfig(searchConfig) { const gemini = searchConfig?.gemini; return isRecord(gemini) ? gemini : {}; } function resolveGeminiModel(gemini) { return normalizeOptionalString(gemini?.model) ?? DEFAULT_GEMINI_WEB_SEARCH_MODEL; } function resolveGeminiBaseUrl(gemini) { return normalizeGoogleApiBaseUrl(normalizeOptionalString(gemini?.baseUrl) ?? normalizeOptionalString(gemini?.providerBaseUrl)); } //#endregion //#region extensions/google/src/gemini-web-search-provider.runtime.ts const GEMINI_PROVIDER_OWNED_HEADER_NAMES = /* @__PURE__ */ new Set([ "content-type", "x-goog-api-client", "x-goog-api-key" ]); const GEMINI_UNSAFE_REQUEST_HEADER_NAMES = /* @__PURE__ */ new Set([ "connection", "content-length", "expect", "host", "keep-alive", "proxy-connection", "te", "trailer", "transfer-encoding", "upgrade" ]); function throwMalformedGeminiResponse() { throw new Error("Gemini API error: malformed JSON response"); } const GEMINI_FRESHNESS_DAYS = { day: 1, week: 7, month: 30, year: 365 }; const GEMINI_DAY_FRESHNESS_HINT = "Prioritize web sources published in the last 24 hours."; function toGeminiTimeRangeTimestamp(date) { return date.toISOString().replace(/\.\d+Z$/, "Z"); } function isoDateStart(value) { return `${value}T00:00:00Z`; } function isoDateExclusiveEnd(value) { const end = /* @__PURE__ */ new Date(`${value}T00:00:00Z`); end.setUTCDate(end.getUTCDate() + 1); return toGeminiTimeRangeTimestamp(end); } function freshnessStartTime(freshness, now) { const start = new Date(now); start.setUTCDate(start.getUTCDate() - GEMINI_FRESHNESS_DAYS[freshness]); return toGeminiTimeRangeTimestamp(start); } function queryWithSoftFreshness(query, freshness) { if (freshness !== "day") return query; return `${query}\n\nSearch recency instruction: ${GEMINI_DAY_FRESHNESS_HINT} If no matching recent sources are available, state that limitation and use the most relevant available sources.`; } function resolveGeminiTimeRangeFilter(args, now = /* @__PURE__ */ new Date()) { const rawFreshness = readToolStringParam(args, "freshness"); const rawDateAfter = readToolStringParam(args, "date_after"); const rawDateBefore = readToolStringParam(args, "date_before"); const parsedTimeFilters = parseWebSearchTimeFilters({ rawDateAfter, rawDateBefore, rawFreshness, freshnessProvider: "perplexity", invalidFreshnessMessage: "freshness must be day, week, month, year, or the shortcuts pd, pw, pm, py.", invalidDateAfterMessage: "date_after must be YYYY-MM-DD format.", invalidDateBeforeMessage: "date_before must be YYYY-MM-DD format.", invalidDateRangeMessage: "date_after must be before date_before." }); if ("error" in parsedTimeFilters) return parsedTimeFilters; const { freshness, dateAfter, dateBefore } = parsedTimeFilters; if (freshness) { if (freshness === "day") return { freshness }; return { freshness, timeRangeFilter: { startTime: freshnessStartTime(freshness, now), endTime: toGeminiTimeRangeTimestamp(now) } }; } if (!dateAfter && !dateBefore) return {}; return { dateAfter, dateBefore, timeRangeFilter: { startTime: dateAfter ? isoDateStart(dateAfter) : "1970-01-01T00:00:00Z", endTime: dateBefore ? isoDateExclusiveEnd(dateBefore) : toGeminiTimeRangeTimestamp(now) } }; } function resolveGeminiRuntimeApiKey(gemini) { return readConfiguredSecretString(gemini?.apiKey, "plugins.entries.google.config.webSearch.apiKey") ?? readProviderEnvValue(["GEMINI_API_KEY"]) ?? readConfiguredSecretString(gemini?.providerApiKey, "models.providers.google.apiKey"); } function resolveGeminiWebSearchHeaders(gemini) { if (!isRecord(gemini?.headers)) return; const headers = new Headers(); for (const [name, input] of Object.entries(gemini.headers)) { const path = `plugins.entries.google.config.webSearch.headers[${JSON.stringify(name)}]`; const value = typeof input === "string" ? input : normalizeResolvedSecretInputString({ value: input, path }); if (value === void 0) throw new Error(`${path} must be a string or resolved SecretRef.`); let normalizedName; let normalizedValue; try { const [entry] = new Headers([[name, value]]).entries(); if (!entry) throw new Error("missing normalized header entry"); [normalizedName, normalizedValue] = entry; } catch { throw new Error(`${path} is not a valid HTTP header.`); } if (GEMINI_UNSAFE_REQUEST_HEADER_NAMES.has(normalizedName)) throw new Error(`${path} uses a reserved or framing HTTP header.`); if (GEMINI_PROVIDER_OWNED_HEADER_NAMES.has(normalizedName)) continue; headers.set(normalizedName, normalizedValue); } const entries = [...headers.entries()]; return entries.length > 0 ? Object.fromEntries(entries) : void 0; } function buildGeminiRequestHeaders(params) { const providerHeaders = { "Content-Type": "application/json", "x-goog-api-key": params.apiKey, ...resolveGoogleApiClientHeaders({ baseUrl: params.baseUrl, api: "google-generative-ai", capability: "other", transport: "http" }) }; if (!params.operatorHeaders) return providerHeaders; const headers = new Headers(params.operatorHeaders); for (const [name, value] of Object.entries(providerHeaders)) headers.set(name, value); return headers; } async function runGeminiSearch(params) { const endpoint = `${params.baseUrl}/models/${params.model}:generateContent`; const googleSearch = params.timeRangeFilter === void 0 ? {} : { timeRangeFilter: params.timeRangeFilter }; return withTrustedWebSearchEndpoint({ url: endpoint, timeoutSeconds: params.timeoutSeconds, signal: params.signal, init: { method: "POST", headers: buildGeminiRequestHeaders({ apiKey: params.apiKey, baseUrl: params.baseUrl, operatorHeaders: params.headers }), body: JSON.stringify({ contents: [{ parts: [{ text: params.query }] }], tools: [{ google_search: googleSearch }] }) } }, async (res) => { if (!res.ok) { const error = await createProviderHttpError(res, "Gemini API error"); throw new Error(error.message.replace(/key=[^&\s]+/giu, "key=***")); } const data = await readProviderJsonObjectResponse(res, "Gemini API error"); if (data.error !== void 0) { if (!isRecord(data.error)) throwMalformedGeminiResponse(); const rawMessage = normalizeOptionalString(data.error.message) ?? normalizeOptionalString(data.error.status) ?? "unknown"; throw new Error(formatProviderHttpErrorMessage({ label: "Gemini API error", status: typeof data.error.code === "number" ? data.error.code : 0, detail: rawMessage.replace(/key=[^&\s]+/giu, "key=***") })); } if (data.candidates !== void 0 && !Array.isArray(data.candidates) || data.promptFeedback !== void 0 && !isRecord(data.promptFeedback)) throwMalformedGeminiResponse(); const candidate = data.candidates?.[0]; if (candidate !== void 0 && !isRecord(candidate)) throwMalformedGeminiResponse(); const candidateContent = candidate?.content; if (candidateContent !== void 0 && !isRecord(candidateContent)) throwMalformedGeminiResponse(); const parts = candidateContent?.parts; if (parts !== void 0 && !Array.isArray(parts)) throwMalformedGeminiResponse(); const content = (parts ?? []).map((part) => isRecord(part) && typeof part.text === "string" ? part.text : void 0).filter((text) => Boolean(text)).join("\n"); const groundingMetadata = candidate?.groundingMetadata; if (groundingMetadata !== void 0 && !isRecord(groundingMetadata)) throwMalformedGeminiResponse(); const groundingChunks = groundingMetadata?.groundingChunks; if (groundingChunks !== void 0 && !Array.isArray(groundingChunks)) throwMalformedGeminiResponse(); if (!content) { const reasons = [data.promptFeedback?.blockReason, candidate?.finishReason]; if (reasons.some((reason) => reason !== void 0 && typeof reason !== "string") || parts?.some((part) => !isRecord(part) || part.text !== void 0 && typeof part.text !== "string")) throwMalformedGeminiResponse(); const reason = reasons.map((value) => normalizeOptionalString(value)).find(Boolean); const detail = reason ? ` (${truncateErrorDetail(reason, 120)})` : ""; throw new Error(`Gemini search returned no final answer${detail}.`); } const rawCitations = (groundingChunks ?? []).flatMap((chunk) => { if (!isRecord(chunk) || !isRecord(chunk.web) || typeof chunk.web.uri !== "string") return []; return [{ url: chunk.web.uri, title: typeof chunk.web.title === "string" ? chunk.web.title : void 0 }]; }); const citations = []; for (let index = 0; index < rawCitations.length; index += 10) { const batch = rawCitations.slice(index, index + 10); const resolved = await Promise.all(batch.map(async (citation) => Object.assign({}, citation, { url: await resolveCitationRedirectUrl(citation.url) }))); citations.push(...resolved); } return { content, citations }; }); } async function executeGeminiSearch(args, searchConfig, context) { context?.signal?.throwIfAborted(); const unsupportedResponse = buildUnsupportedSearchFilterResponse({ country: args.country, language: args.language }, "gemini"); if (unsupportedResponse) return unsupportedResponse; const timeRange = resolveGeminiTimeRangeFilter(args); if ("error" in timeRange) return timeRange; const geminiConfig = resolveGeminiConfig(searchConfig); const apiKey = resolveGeminiRuntimeApiKey(geminiConfig); if (!apiKey) return { error: "missing_gemini_api_key", message: "web_search (gemini) needs an API key. Set GEMINI_API_KEY in the Gateway environment, configure plugins.entries.google.config.webSearch.apiKey, or reuse models.providers.google.apiKey. If you do not want to configure a search API key, use web_fetch for a specific URL or the browser tool for interactive pages.", docs: "https://docs.openclaw.ai/tools/web" }; const query = readToolStringParam(args, "query", { required: true }); readPositiveIntegerParam(args, "count", { max: 10, message: `count must be an integer from 1 to 10.` }); const model = resolveGeminiModel(geminiConfig); const baseUrl = resolveGeminiBaseUrl(geminiConfig); const headers = resolveGeminiWebSearchHeaders(geminiConfig); const headersCacheKey = headers ? createHash("sha256").update(JSON.stringify(Object.entries(headers).toSorted(([left], [right]) => left.localeCompare(right)))).digest("hex") : void 0; const cacheKey = buildSearchCacheKey([ "gemini", query, baseUrl, model, timeRange.freshness, timeRange.dateAfter, timeRange.dateBefore, headersCacheKey ]); const cacheTtlMs = resolveSearchCacheTtlMs(searchConfig); const cached = readCachedSearchPayload(cacheKey, cacheTtlMs); if (cached) return cached; const start = Date.now(); const result = await runGeminiSearch({ query: queryWithSoftFreshness(query, timeRange.freshness), apiKey, baseUrl, model, timeoutSeconds: resolveSearchTimeoutSeconds(searchConfig), signal: context?.signal, timeRangeFilter: timeRange.timeRangeFilter, headers }); context?.signal?.throwIfAborted(); const payload = { query, provider: "gemini", model, tookMs: Date.now() - start, externalContent: { untrusted: true, source: "web_search", provider: "gemini", wrapped: true }, content: wrapWebContent(result.content), citations: result.citations }; writeCachedSearchPayload(cacheKey, payload, cacheTtlMs); return payload; } //#endregion export { executeGeminiSearch };