openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
186 lines (185 loc) • 8.22 kB
JavaScript
import { c as isRecord } from "./utils-CCC-BEJH.js";
import { o as createProviderHttpError, p as readProviderJsonObjectResponse, u as formatProviderHttpErrorMessage } from "./provider-http-errors-DqaqQLLZ.js";
import { b as readStringParam, g as readPositiveIntegerParam } from "./common-C4yy9V-D.js";
import { a as wrapWebContent } from "./external-content-pX-Pk1Iu.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import { S as writeCachedSearchPayload, _ as resolveSearchTimeoutSeconds, a as buildUnsupportedSearchFilterResponse, f as readCachedSearchPayload, g as resolveSearchCount, h as resolveSearchCacheTtlMs, i as buildSearchCacheKey, m as readProviderEnvValue, p as readConfiguredSecretString, u as parseWebSearchTimeFilters, x as withTrustedWebSearchEndpoint } from "./web-search-provider-common-DisZTXTb.js";
import "./provider-http-BXCBCi4E.js";
import { r as resolveCitationRedirectUrl } from "./provider-web-search-9G87vZMY.js";
import { n as resolveGeminiConfig, r as resolveGeminiModel, t as resolveGeminiBaseUrl } from "./gemini-web-search-provider.shared-B1XU0mP6.js";
//#region extensions/google/src/gemini-web-search-provider.runtime.ts
function throwMalformedGeminiResponse() {
throw new Error("Gemini API error: malformed JSON response");
}
const GEMINI_FRESHNESS_DAYS = {
day: 1,
week: 7,
month: 30,
year: 365
};
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 resolveGeminiTimeRangeFilter(args, now = /* @__PURE__ */ new Date()) {
const rawFreshness = readStringParam(args, "freshness");
const parsedTimeFilters = parseWebSearchTimeFilters({
rawDateAfter: readStringParam(args, "date_after"),
rawDateBefore: readStringParam(args, "date_before"),
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) return { timeRangeFilter: {
startTime: freshnessStartTime(freshness, now),
endTime: toGeminiTimeRangeTimestamp(now)
} };
if (!dateAfter && !dateBefore) return {};
return { timeRangeFilter: {
startTime: dateAfter ? isoDateStart(dateAfter) : "1970-01-01T00:00:00Z",
endTime: dateBefore ? isoDateExclusiveEnd(dateBefore) : toGeminiTimeRangeTimestamp(now)
} };
}
function resolveGeminiRuntimeApiKey(gemini) {
return readConfiguredSecretString(gemini?.apiKey, "tools.web.search.gemini.apiKey") ?? readProviderEnvValue(["GEMINI_API_KEY"]) ?? readConfiguredSecretString(gemini?.providerApiKey, "models.providers.google.apiKey");
}
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: {
"Content-Type": "application/json",
"x-goog-api-key": params.apiKey
},
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) {
const rawMessage = data.error.message || data.error.status || "unknown";
throw new Error(formatProviderHttpErrorMessage({
label: "Gemini API error",
status: data.error.code ?? 0,
detail: rawMessage.replace(/key=[^&\s]+/giu, "key=***")
}));
}
if (!Array.isArray(data.candidates)) throwMalformedGeminiResponse();
const candidate = data.candidates[0];
if (!isRecord(candidate) || !isRecord(candidate.content)) throwMalformedGeminiResponse();
const parts = candidate.content.parts;
if (!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");
if (!content) throwMalformedGeminiResponse();
const groundingMetadata = candidate.groundingMetadata;
const groundingChunks = groundingMetadata === void 0 ? [] : isRecord(groundingMetadata) ? groundingMetadata.groundingChunks === void 0 ? [] : Array.isArray(groundingMetadata.groundingChunks) ? groundingMetadata.groundingChunks : void 0 : void 0;
if (!groundingChunks) throwMalformedGeminiResponse();
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) {
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 = readStringParam(args, "query", { required: true });
const count = readPositiveIntegerParam(args, "count", {
max: 10,
message: `count must be an integer from 1 to 10.`
}) ?? searchConfig?.maxResults ?? void 0;
const model = resolveGeminiModel(geminiConfig);
const baseUrl = resolveGeminiBaseUrl(geminiConfig);
const cacheKey = buildSearchCacheKey([
"gemini",
query,
resolveSearchCount(count, 5),
baseUrl,
model,
timeRange.timeRangeFilter?.startTime,
timeRange.timeRangeFilter?.endTime
]);
const cached = readCachedSearchPayload(cacheKey);
if (cached) return cached;
const start = Date.now();
const result = await runGeminiSearch({
query,
apiKey,
baseUrl,
model,
timeoutSeconds: resolveSearchTimeoutSeconds(searchConfig),
signal: context?.signal,
timeRangeFilter: timeRange.timeRangeFilter
});
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, resolveSearchCacheTtlMs(searchConfig));
return payload;
}
//#endregion
export { executeGeminiSearch, resolveGeminiRuntimeApiKey };