openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
308 lines (307 loc) • 12.7 kB
JavaScript
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js";
import { _ as uniqueStrings } from "./string-normalization-WNUDCpXX.js";
import { c as isRecord } from "./utils-CCC-BEJH.js";
import { o as createProviderHttpError, p as readProviderJsonObjectResponse } 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, x as withTrustedWebSearchEndpoint } from "./web-search-provider-common-DisZTXTb.js";
import "./provider-http-BXCBCi4E.js";
import { a as setProviderWebSearchPluginConfigValue, i as resolveProviderWebSearchPluginConfig, r as mergeScopedSearchConfig } from "./web-search-provider-config-BQzMMhw8.js";
import "./provider-web-search-9G87vZMY.js";
import { n as MOONSHOT_CN_BASE_URL, o as isNativeMoonshotBaseUrl, r as MOONSHOT_DEFAULT_MODEL_ID, t as MOONSHOT_BASE_URL } from "./provider-catalog-C_q7hjV_.js";
//#region extensions/moonshot/src/kimi-web-search-provider.runtime.ts
const DEFAULT_KIMI_BASE_URL = MOONSHOT_BASE_URL;
const DEFAULT_KIMI_SEARCH_MODEL = MOONSHOT_DEFAULT_MODEL_ID;
/** Models that require explicit thinking disablement for web search.
* Reasoning variants (kimi-k2-thinking, kimi-k2-thinking-turbo) are excluded
* because they default to thinking-enabled and disabling it would defeat their
* purpose; they are also unlikely to be used for web search. */
const KIMI_THINKING_MODELS = new Set(["kimi-k2.6", "kimi-k2.5"]);
const KIMI_WEB_SEARCH_TOOL = {
type: "builtin_function",
function: { name: "$web_search" }
};
function throwMalformedKimiResponse() {
throw new Error("Kimi API error: malformed JSON response");
}
function resolveKimiConfig(searchConfig) {
const kimi = searchConfig?.kimi;
return kimi && typeof kimi === "object" && !Array.isArray(kimi) ? kimi : {};
}
function resolveKimiApiKey(kimi) {
return readConfiguredSecretString(kimi?.apiKey, "tools.web.search.kimi.apiKey") ?? readProviderEnvValue(["KIMI_API_KEY", "MOONSHOT_API_KEY"]);
}
function resolveKimiModel(kimi) {
return (normalizeOptionalString(kimi?.model) ?? "") || DEFAULT_KIMI_SEARCH_MODEL;
}
function trimTrailingSlashes(url) {
return url.replace(/\/+$/, "");
}
function resolveKimiBaseUrl(kimi, openClawConfig) {
const explicitBaseUrl = normalizeOptionalString(kimi?.baseUrl) ?? "";
if (explicitBaseUrl) return trimTrailingSlashes(explicitBaseUrl) || DEFAULT_KIMI_BASE_URL;
const moonshotBaseUrl = openClawConfig?.models?.providers?.moonshot?.baseUrl;
if (typeof moonshotBaseUrl === "string") {
const normalizedMoonshotBaseUrl = trimTrailingSlashes(moonshotBaseUrl.trim());
if (normalizedMoonshotBaseUrl && isNativeMoonshotBaseUrl(normalizedMoonshotBaseUrl)) return normalizedMoonshotBaseUrl;
}
return DEFAULT_KIMI_BASE_URL;
}
function extractKimiMessageText(message) {
const content = message?.content?.trim();
if (content) return content;
return message?.reasoning_content?.trim() || void 0;
}
function extractKimiCitations(data) {
const searchResults = data.search_results ?? [];
if (!Array.isArray(searchResults)) throwMalformedKimiResponse();
const citations = searchResults.map((entry) => isRecord(entry) && typeof entry.url === "string" ? entry.url.trim() : "").filter((url) => Boolean(url));
const choices = data.choices ?? [];
if (!Array.isArray(choices)) throwMalformedKimiResponse();
const firstChoice = choices[0];
const toolCalls = (firstChoice && isRecord(firstChoice.message) ? firstChoice.message : void 0)?.tool_calls ?? [];
if (!Array.isArray(toolCalls)) throwMalformedKimiResponse();
for (const toolCall of toolCalls) {
if (!isRecord(toolCall) || !isRecord(toolCall.function)) continue;
const rawArguments = toolCall.function.arguments;
if (typeof rawArguments !== "string" || !rawArguments) continue;
try {
const parsed = JSON.parse(rawArguments);
const parsedUrl = normalizeOptionalString(parsed.url);
if (parsedUrl) citations.push(parsedUrl);
for (const result of parsed.search_results ?? []) {
const resultUrl = normalizeOptionalString(result.url);
if (resultUrl) citations.push(resultUrl);
}
} catch {}
}
return uniqueStrings(citations);
}
function hasKimiSearchResults(data) {
const searchResults = data.search_results ?? [];
if (!Array.isArray(searchResults)) throwMalformedKimiResponse();
return searchResults.some((entry) => isRecord(entry) && (Boolean(normalizeOptionalString(entry.url)) || Boolean(normalizeOptionalString(entry.title)) || Boolean(normalizeOptionalString(entry.content))));
}
function extractKimiToolResultContent(toolCall) {
const rawArguments = toolCall.function?.arguments;
if (typeof rawArguments !== "string" || rawArguments.trim().length === 0) return;
return rawArguments;
}
async function runKimiSearch(params) {
const endpoint = `${params.baseUrl.trim().replace(/\/$/, "")}/chat/completions`;
const messages = [{
role: "user",
content: params.query
}];
const collectedCitations = /* @__PURE__ */ new Set();
let hasGroundingEvidence = false;
for (let round = 0; round < 3; round += 1) {
const next = await withTrustedWebSearchEndpoint({
url: endpoint,
timeoutSeconds: params.timeoutSeconds,
init: {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${params.apiKey}`
},
body: JSON.stringify({
model: params.model,
...KIMI_THINKING_MODELS.has(params.model) ? { thinking: { type: "disabled" } } : {},
messages,
tools: [KIMI_WEB_SEARCH_TOOL]
})
}
}, async (res) => {
if (!res.ok) throw await createProviderHttpError(res, "Kimi API error");
const data = await readProviderJsonObjectResponse(res, "Kimi API error");
if (!Array.isArray(data.choices)) throwMalformedKimiResponse();
if (hasKimiSearchResults(data)) hasGroundingEvidence = true;
for (const citation of extractKimiCitations(data)) collectedCitations.add(citation);
if (collectedCitations.size > 0) hasGroundingEvidence = true;
const choice = data.choices?.[0];
if (!isRecord(choice) || !isRecord(choice.message)) throwMalformedKimiResponse();
const message = choice?.message;
const text = extractKimiMessageText(message);
const toolCalls = message?.tool_calls ?? [];
if (!Array.isArray(toolCalls)) throwMalformedKimiResponse();
if (choice?.finish_reason !== "tool_calls" || toolCalls.length === 0) {
if (!text) throwMalformedKimiResponse();
return {
done: true,
content: text,
citations: [...collectedCitations]
};
}
messages.push({
role: "assistant",
content: message?.content ?? "",
...message?.reasoning_content ? { reasoning_content: message.reasoning_content } : {},
tool_calls: toolCalls
});
let pushed = false;
for (const toolCall of toolCalls) {
const toolCallId = toolCall.id?.trim();
const toolCallName = toolCall.function?.name?.trim();
const toolContent = extractKimiToolResultContent(toolCall);
if (!toolCallId || !toolCallName || !toolContent) continue;
if (toolCallName === KIMI_WEB_SEARCH_TOOL.function.name) hasGroundingEvidence = true;
pushed = true;
messages.push({
role: "tool",
tool_call_id: toolCallId,
name: toolCallName,
content: toolContent
});
}
if (!pushed) {
if (!text) throwMalformedKimiResponse();
return {
done: true,
content: text,
citations: [...collectedCitations]
};
}
return { done: false };
});
if (next.done) return {
content: next.content,
citations: next.citations,
grounded: hasGroundingEvidence
};
}
return {
content: "Search completed but no final answer was produced.",
citations: [...collectedCitations],
grounded: hasGroundingEvidence
};
}
async function executeKimiWebSearchProviderTool(ctx, args) {
const searchConfig = mergeScopedSearchConfig(ctx.searchConfig, "kimi", resolveProviderWebSearchPluginConfig(ctx.config, "moonshot"));
const unsupportedResponse = buildUnsupportedSearchFilterResponse(args, "kimi");
if (unsupportedResponse) return unsupportedResponse;
const kimiConfig = resolveKimiConfig(searchConfig);
const apiKey = resolveKimiApiKey(kimiConfig);
if (!apiKey) return {
error: "missing_kimi_api_key",
message: "web_search (kimi) needs a Moonshot API key. Set KIMI_API_KEY or MOONSHOT_API_KEY in the Gateway environment, or configure tools.web.search.kimi.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 = resolveKimiModel(kimiConfig);
const baseUrl = resolveKimiBaseUrl(kimiConfig, ctx.config);
const cacheKey = buildSearchCacheKey([
"kimi",
query,
resolveSearchCount(count, 5),
baseUrl,
model
]);
const cached = readCachedSearchPayload(cacheKey);
if (cached) return cached;
const start = Date.now();
const result = await runKimiSearch({
query,
apiKey,
baseUrl,
model,
timeoutSeconds: resolveSearchTimeoutSeconds(searchConfig)
});
if (!result.grounded) return {
error: "kimi_web_search_ungrounded",
message: "Kimi returned a chat completion without native web-search grounding. Retry the query, switch to a structured provider such as Brave, or use web_fetch/browser for a specific URL.",
query,
provider: "kimi",
model,
docs: "https://docs.openclaw.ai/tools/kimi-search",
tookMs: Date.now() - start
};
const payload = {
query,
provider: "kimi",
model,
tookMs: Date.now() - start,
externalContent: {
untrusted: true,
source: "web_search",
provider: "kimi",
wrapped: true
},
content: wrapWebContent(result.content),
citations: result.citations
};
writeCachedSearchPayload(cacheKey, payload, resolveSearchCacheTtlMs(searchConfig));
return payload;
}
async function runKimiSearchProviderSetup(ctx) {
const existingPluginConfig = resolveProviderWebSearchPluginConfig(ctx.config, "moonshot");
const normalizedBaseUrl = (normalizeOptionalString(existingPluginConfig?.baseUrl) ?? "").replace(/\/+$/, "");
const existingModel = normalizeOptionalString(existingPluginConfig?.model) ?? "";
const isCustomBaseUrl = normalizedBaseUrl && !isNativeMoonshotBaseUrl(normalizedBaseUrl);
const regionOptions = [];
if (isCustomBaseUrl) regionOptions.push({
value: normalizedBaseUrl,
label: `Keep current (${normalizedBaseUrl})`,
hint: "custom endpoint"
});
regionOptions.push({
value: MOONSHOT_BASE_URL,
label: "Moonshot API key (.ai)",
hint: "api.moonshot.ai"
}, {
value: MOONSHOT_CN_BASE_URL,
label: "Moonshot API key (.cn)",
hint: "api.moonshot.cn"
});
const baseUrl = await ctx.prompter.select({
message: "Kimi API region",
options: regionOptions,
initialValue: normalizedBaseUrl || "https://api.moonshot.ai/v1"
});
const currentModelLabel = existingModel ? `Keep current (moonshot/${existingModel})` : `Use default (moonshot/${DEFAULT_KIMI_SEARCH_MODEL})`;
const modelChoice = await ctx.prompter.select({
message: "Kimi web search model",
options: [
{
value: "__keep__",
label: currentModelLabel
},
{
value: "__custom__",
label: "Enter model manually"
},
{
value: DEFAULT_KIMI_SEARCH_MODEL,
label: `moonshot/${DEFAULT_KIMI_SEARCH_MODEL}`
}
],
initialValue: "__keep__"
});
let model;
if (modelChoice === "__keep__") model = existingModel || DEFAULT_KIMI_SEARCH_MODEL;
else if (modelChoice === "__custom__") model = (await ctx.prompter.text({
message: "Kimi model name",
initialValue: existingModel || DEFAULT_KIMI_SEARCH_MODEL,
placeholder: DEFAULT_KIMI_SEARCH_MODEL
}))?.trim() || DEFAULT_KIMI_SEARCH_MODEL;
else model = modelChoice;
const next = { ...ctx.config };
setProviderWebSearchPluginConfigValue(next, "moonshot", "baseUrl", baseUrl);
setProviderWebSearchPluginConfigValue(next, "moonshot", "model", model);
return next;
}
const testing = {
resolveKimiApiKey,
resolveKimiModel,
resolveKimiBaseUrl,
extractKimiCitations,
hasKimiSearchResults,
extractKimiToolResultContent
};
//#endregion
export { testing as __testing, testing, executeKimiWebSearchProviderTool, runKimiSearchProviderSetup };