fix: restore full gate after web-search rebase
This commit is contained in:
parent
861fcb1575
commit
e9b19ca1d1
@ -16,8 +16,8 @@ import {
|
||||
resolveSearchTimeoutSeconds,
|
||||
resolveSiteName,
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
setTopLevelCredentialValue,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
type OpenClawConfig,
|
||||
type SearchConfigRecord,
|
||||
type WebSearchProviderPlugin,
|
||||
type WebSearchProviderToolDefinition,
|
||||
@ -92,7 +92,6 @@ const BRAVE_SEARCH_LANG_ALIASES: Record<string, string> = {
|
||||
const BRAVE_UI_LANG_LOCALE = /^([a-z]{2})-([a-z]{2})$/i;
|
||||
|
||||
type BraveConfig = {
|
||||
apiKey?: unknown;
|
||||
mode?: string;
|
||||
};
|
||||
|
||||
@ -115,41 +114,18 @@ type BraveLlmContextResponse = {
|
||||
sources?: { url?: string; hostname?: string; date?: string }[];
|
||||
};
|
||||
|
||||
function resolveBraveConfig(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): BraveConfig {
|
||||
const pluginConfig = resolveProviderWebSearchPluginConfig(config, "brave");
|
||||
if (pluginConfig) {
|
||||
return pluginConfig as BraveConfig;
|
||||
}
|
||||
const scoped = (searchConfig as Record<string, unknown> | undefined)?.brave;
|
||||
return scoped && typeof scoped === "object" && !Array.isArray(scoped)
|
||||
? ({
|
||||
...(scoped as BraveConfig),
|
||||
apiKey: (searchConfig as Record<string, unknown> | undefined)?.apiKey,
|
||||
} as BraveConfig)
|
||||
: ({ apiKey: (searchConfig as Record<string, unknown> | undefined)?.apiKey } as BraveConfig);
|
||||
function resolveBraveConfig(searchConfig?: SearchConfigRecord): BraveConfig {
|
||||
const brave = searchConfig?.brave;
|
||||
return brave && typeof brave === "object" && !Array.isArray(brave) ? (brave as BraveConfig) : {};
|
||||
}
|
||||
|
||||
function resolveBraveMode(brave?: BraveConfig): "web" | "llm-context" {
|
||||
return brave?.mode === "llm-context" ? "llm-context" : "web";
|
||||
}
|
||||
|
||||
function resolveBraveApiKey(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): string | undefined {
|
||||
const braveConfig = resolveBraveConfig(config, searchConfig);
|
||||
function resolveBraveApiKey(searchConfig?: SearchConfigRecord): string | undefined {
|
||||
return (
|
||||
readConfiguredSecretString(
|
||||
braveConfig.apiKey,
|
||||
"plugins.entries.brave.config.webSearch.apiKey",
|
||||
) ??
|
||||
readConfiguredSecretString(
|
||||
(searchConfig as Record<string, unknown> | undefined)?.apiKey,
|
||||
"tools.web.search.apiKey",
|
||||
) ??
|
||||
readConfiguredSecretString(searchConfig?.apiKey, "tools.web.search.apiKey") ??
|
||||
readProviderEnvValue(["BRAVE_API_KEY"])
|
||||
);
|
||||
}
|
||||
@ -410,10 +386,9 @@ function missingBraveKeyPayload() {
|
||||
}
|
||||
|
||||
function createBraveToolDefinition(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): WebSearchProviderToolDefinition {
|
||||
const braveConfig = resolveBraveConfig(config, searchConfig);
|
||||
const braveConfig = resolveBraveConfig(searchConfig);
|
||||
const braveMode = resolveBraveMode(braveConfig);
|
||||
|
||||
return {
|
||||
@ -423,7 +398,7 @@ function createBraveToolDefinition(
|
||||
: "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.",
|
||||
parameters: createBraveSchema(),
|
||||
execute: async (args) => {
|
||||
const apiKey = resolveBraveApiKey(config, searchConfig);
|
||||
const apiKey = resolveBraveApiKey(searchConfig);
|
||||
if (!apiKey) {
|
||||
return missingBraveKeyPayload();
|
||||
}
|
||||
@ -624,16 +599,20 @@ export function createBraveWebSearchProvider(): WebSearchProviderPlugin {
|
||||
credentialPath: "plugins.entries.brave.config.webSearch.apiKey",
|
||||
inactiveSecretPaths: ["plugins.entries.brave.config.webSearch.apiKey"],
|
||||
getCredentialValue: (searchConfig) => searchConfig?.apiKey,
|
||||
setCredentialValue: (searchConfigTarget, value) => {
|
||||
searchConfigTarget.apiKey = value;
|
||||
},
|
||||
setCredentialValue: setTopLevelCredentialValue,
|
||||
getConfiguredCredentialValue: (config) =>
|
||||
resolveProviderWebSearchPluginConfig(config, "brave")?.apiKey,
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
setProviderWebSearchPluginConfigValue(configTarget, "brave", "apiKey", value);
|
||||
},
|
||||
createTool: (ctx) =>
|
||||
createBraveToolDefinition(ctx.config, ctx.searchConfig as SearchConfigRecord | undefined),
|
||||
createTool: (ctx) => {
|
||||
const pluginConfig = resolveProviderWebSearchPluginConfig(ctx.config, "brave");
|
||||
const searchConfig = {
|
||||
...(ctx.searchConfig as SearchConfigRecord | undefined),
|
||||
...(pluginConfig as SearchConfigRecord | undefined),
|
||||
};
|
||||
return createBraveToolDefinition(searchConfig);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,6 @@ import {
|
||||
resolveSearchTimeoutSeconds,
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
type OpenClawConfig,
|
||||
type SearchConfigRecord,
|
||||
type WebSearchProviderPlugin,
|
||||
type WebSearchProviderToolDefinition,
|
||||
@ -54,15 +53,8 @@ type GeminiGroundingResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
function resolveGeminiConfig(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): GeminiConfig {
|
||||
const pluginConfig = resolveProviderWebSearchPluginConfig(config, "google");
|
||||
if (pluginConfig) {
|
||||
return pluginConfig as GeminiConfig;
|
||||
}
|
||||
const gemini = (searchConfig as Record<string, unknown> | undefined)?.gemini;
|
||||
function resolveGeminiConfig(searchConfig?: SearchConfigRecord): GeminiConfig {
|
||||
const gemini = searchConfig?.gemini;
|
||||
return gemini && typeof gemini === "object" && !Array.isArray(gemini)
|
||||
? (gemini as GeminiConfig)
|
||||
: {};
|
||||
@ -70,7 +62,7 @@ function resolveGeminiConfig(
|
||||
|
||||
function resolveGeminiApiKey(gemini?: GeminiConfig): string | undefined {
|
||||
return (
|
||||
readConfiguredSecretString(gemini?.apiKey, "plugins.entries.google.config.webSearch.apiKey") ??
|
||||
readConfiguredSecretString(gemini?.apiKey, "tools.web.search.gemini.apiKey") ??
|
||||
readProviderEnvValue(["GEMINI_API_KEY"])
|
||||
);
|
||||
}
|
||||
@ -177,7 +169,6 @@ function createGeminiSchema() {
|
||||
}
|
||||
|
||||
function createGeminiToolDefinition(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): WebSearchProviderToolDefinition {
|
||||
return {
|
||||
@ -204,13 +195,13 @@ function createGeminiToolDefinition(
|
||||
}
|
||||
}
|
||||
|
||||
const geminiConfig = resolveGeminiConfig(config, searchConfig);
|
||||
const geminiConfig = resolveGeminiConfig(searchConfig);
|
||||
const apiKey = resolveGeminiApiKey(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, or configure plugins.entries.google.config.webSearch.apiKey.",
|
||||
"web_search (gemini) needs an API key. Set GEMINI_API_KEY in the Gateway environment, or configure tools.web.search.gemini.apiKey.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
@ -290,8 +281,19 @@ export function createGeminiWebSearchProvider(): WebSearchProviderPlugin {
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
setProviderWebSearchPluginConfigValue(configTarget, "google", "apiKey", value);
|
||||
},
|
||||
createTool: (ctx) =>
|
||||
createGeminiToolDefinition(ctx.config, ctx.searchConfig as SearchConfigRecord | undefined),
|
||||
createTool: (ctx) => {
|
||||
const pluginConfig = resolveProviderWebSearchPluginConfig(ctx.config, "google");
|
||||
const searchConfig = {
|
||||
...(ctx.searchConfig as SearchConfigRecord | undefined),
|
||||
gemini: {
|
||||
...((ctx.searchConfig as SearchConfigRecord | undefined)?.gemini as
|
||||
| Record<string, unknown>
|
||||
| undefined),
|
||||
...(pluginConfig as Record<string, unknown> | undefined),
|
||||
},
|
||||
} as SearchConfigRecord;
|
||||
return createGeminiToolDefinition(searchConfig);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,6 @@ import {
|
||||
resolveSearchTimeoutSeconds,
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
type OpenClawConfig,
|
||||
type SearchConfigRecord,
|
||||
type WebSearchProviderPlugin,
|
||||
type WebSearchProviderToolDefinition,
|
||||
@ -63,18 +62,14 @@ type KimiSearchResponse = {
|
||||
}>;
|
||||
};
|
||||
|
||||
function resolveKimiConfig(config?: OpenClawConfig, searchConfig?: SearchConfigRecord): KimiConfig {
|
||||
const pluginConfig = resolveProviderWebSearchPluginConfig(config, "moonshot");
|
||||
if (pluginConfig) {
|
||||
return pluginConfig as KimiConfig;
|
||||
}
|
||||
const kimi = (searchConfig as Record<string, unknown> | undefined)?.kimi;
|
||||
function resolveKimiConfig(searchConfig?: SearchConfigRecord): KimiConfig {
|
||||
const kimi = searchConfig?.kimi;
|
||||
return kimi && typeof kimi === "object" && !Array.isArray(kimi) ? (kimi as KimiConfig) : {};
|
||||
}
|
||||
|
||||
function resolveKimiApiKey(kimi?: KimiConfig): string | undefined {
|
||||
return (
|
||||
readConfiguredSecretString(kimi?.apiKey, "plugins.entries.moonshot.config.webSearch.apiKey") ??
|
||||
readConfiguredSecretString(kimi?.apiKey, "tools.web.search.kimi.apiKey") ??
|
||||
readProviderEnvValue(["KIMI_API_KEY", "MOONSHOT_API_KEY"])
|
||||
);
|
||||
}
|
||||
@ -243,7 +238,6 @@ function createKimiSchema() {
|
||||
}
|
||||
|
||||
function createKimiToolDefinition(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): WebSearchProviderToolDefinition {
|
||||
return {
|
||||
@ -270,13 +264,13 @@ function createKimiToolDefinition(
|
||||
}
|
||||
}
|
||||
|
||||
const kimiConfig = resolveKimiConfig(config, searchConfig);
|
||||
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 plugins.entries.moonshot.config.webSearch.apiKey.",
|
||||
"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.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
@ -359,8 +353,19 @@ export function createKimiWebSearchProvider(): WebSearchProviderPlugin {
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
setProviderWebSearchPluginConfigValue(configTarget, "moonshot", "apiKey", value);
|
||||
},
|
||||
createTool: (ctx) =>
|
||||
createKimiToolDefinition(ctx.config, ctx.searchConfig as SearchConfigRecord | undefined),
|
||||
createTool: (ctx) => {
|
||||
const pluginConfig = resolveProviderWebSearchPluginConfig(ctx.config, "moonshot");
|
||||
const searchConfig = {
|
||||
...(ctx.searchConfig as SearchConfigRecord | undefined),
|
||||
kimi: {
|
||||
...((ctx.searchConfig as SearchConfigRecord | undefined)?.kimi as
|
||||
| Record<string, unknown>
|
||||
| undefined),
|
||||
...(pluginConfig as Record<string, unknown> | undefined),
|
||||
},
|
||||
} as SearchConfigRecord;
|
||||
return createKimiToolDefinition(searchConfig);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,8 @@ import {
|
||||
readNumberParam,
|
||||
readStringArrayParam,
|
||||
readStringParam,
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import {
|
||||
buildSearchCacheKey,
|
||||
DEFAULT_SEARCH_COUNT,
|
||||
MAX_SEARCH_COUNT,
|
||||
@ -19,7 +21,6 @@ import {
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
throwWebSearchApiError,
|
||||
type OpenClawConfig,
|
||||
type SearchConfigRecord,
|
||||
type WebSearchCredentialResolutionSource,
|
||||
type WebSearchProviderPlugin,
|
||||
@ -70,15 +71,8 @@ type PerplexitySearchApiResponse = {
|
||||
}>;
|
||||
};
|
||||
|
||||
function resolvePerplexityConfig(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): PerplexityConfig {
|
||||
const pluginConfig = resolveProviderWebSearchPluginConfig(config, "perplexity");
|
||||
if (pluginConfig) {
|
||||
return pluginConfig as PerplexityConfig;
|
||||
}
|
||||
const perplexity = (searchConfig as Record<string, unknown> | undefined)?.perplexity;
|
||||
function resolvePerplexityConfig(searchConfig?: SearchConfigRecord): PerplexityConfig {
|
||||
const perplexity = searchConfig?.perplexity;
|
||||
return perplexity && typeof perplexity === "object" && !Array.isArray(perplexity)
|
||||
? (perplexity as PerplexityConfig)
|
||||
: {};
|
||||
@ -104,7 +98,7 @@ function resolvePerplexityApiKey(perplexity?: PerplexityConfig): {
|
||||
} {
|
||||
const fromConfig = readConfiguredSecretString(
|
||||
perplexity?.apiKey,
|
||||
"plugins.entries.perplexity.config.webSearch.apiKey",
|
||||
"tools.web.search.perplexity.apiKey",
|
||||
);
|
||||
if (fromConfig) {
|
||||
return { apiKey: fromConfig, source: "config" };
|
||||
@ -319,16 +313,16 @@ async function runPerplexitySearch(params: {
|
||||
}
|
||||
|
||||
function resolveRuntimeTransport(params: {
|
||||
config?: OpenClawConfig;
|
||||
searchConfig?: Record<string, unknown>;
|
||||
resolvedKey?: string;
|
||||
keySource: WebSearchCredentialResolutionSource;
|
||||
fallbackEnvVar?: string;
|
||||
}): PerplexityTransport | undefined {
|
||||
const scoped = resolvePerplexityConfig(
|
||||
params.config,
|
||||
params.searchConfig as SearchConfigRecord | undefined,
|
||||
);
|
||||
const perplexity = params.searchConfig?.perplexity;
|
||||
const scoped =
|
||||
perplexity && typeof perplexity === "object" && !Array.isArray(perplexity)
|
||||
? (perplexity as { baseUrl?: string; model?: string })
|
||||
: undefined;
|
||||
const configuredBaseUrl = typeof scoped?.baseUrl === "string" ? scoped.baseUrl.trim() : "";
|
||||
const configuredModel = typeof scoped?.model === "string" ? scoped.model.trim() : "";
|
||||
const baseUrl = (() => {
|
||||
@ -410,11 +404,10 @@ function createPerplexitySchema(transport?: PerplexityTransport) {
|
||||
}
|
||||
|
||||
function createPerplexityToolDefinition(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
runtimeTransport?: PerplexityTransport,
|
||||
): WebSearchProviderToolDefinition {
|
||||
const perplexityConfig = resolvePerplexityConfig(config, searchConfig);
|
||||
const perplexityConfig = resolvePerplexityConfig(searchConfig);
|
||||
const schemaTransport =
|
||||
runtimeTransport ??
|
||||
(perplexityConfig.baseUrl || perplexityConfig.model ? "chat_completions" : undefined);
|
||||
@ -431,7 +424,7 @@ function createPerplexityToolDefinition(
|
||||
return {
|
||||
error: "missing_perplexity_api_key",
|
||||
message:
|
||||
"web_search (perplexity) needs an API key. Set PERPLEXITY_API_KEY or OPENROUTER_API_KEY in the Gateway environment, or configure plugins.entries.perplexity.config.webSearch.apiKey.",
|
||||
"web_search (perplexity) needs an API key. Set PERPLEXITY_API_KEY or OPENROUTER_API_KEY in the Gateway environment, or configure tools.web.search.perplexity.apiKey.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
@ -686,19 +679,38 @@ export function createPerplexityWebSearchProvider(): WebSearchProviderPlugin {
|
||||
},
|
||||
resolveRuntimeMetadata: (ctx) => ({
|
||||
perplexityTransport: resolveRuntimeTransport({
|
||||
config: ctx.config,
|
||||
searchConfig: ctx.searchConfig,
|
||||
searchConfig: {
|
||||
...(ctx.searchConfig as SearchConfigRecord | undefined),
|
||||
perplexity: {
|
||||
...((ctx.searchConfig as SearchConfigRecord | undefined)?.perplexity as
|
||||
| Record<string, unknown>
|
||||
| undefined),
|
||||
...(resolveProviderWebSearchPluginConfig(ctx.config, "perplexity") as
|
||||
| Record<string, unknown>
|
||||
| undefined),
|
||||
},
|
||||
},
|
||||
resolvedKey: ctx.resolvedCredential?.value,
|
||||
keySource: ctx.resolvedCredential?.source ?? "missing",
|
||||
fallbackEnvVar: ctx.resolvedCredential?.fallbackEnvVar,
|
||||
}),
|
||||
}),
|
||||
createTool: (ctx) =>
|
||||
createPerplexityToolDefinition(
|
||||
ctx.config,
|
||||
ctx.searchConfig as SearchConfigRecord | undefined,
|
||||
createTool: (ctx) => {
|
||||
const pluginConfig = resolveProviderWebSearchPluginConfig(ctx.config, "perplexity");
|
||||
const searchConfig = {
|
||||
...(ctx.searchConfig as SearchConfigRecord | undefined),
|
||||
perplexity: {
|
||||
...((ctx.searchConfig as SearchConfigRecord | undefined)?.perplexity as
|
||||
| Record<string, unknown>
|
||||
| undefined),
|
||||
...(pluginConfig as Record<string, unknown> | undefined),
|
||||
},
|
||||
} as SearchConfigRecord;
|
||||
return createPerplexityToolDefinition(
|
||||
searchConfig,
|
||||
ctx.runtimeMetadata?.perplexityTransport as PerplexityTransport | undefined,
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,6 @@ import {
|
||||
resolveSearchTimeoutSeconds,
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
type OpenClawConfig,
|
||||
type SearchConfigRecord,
|
||||
type WebSearchProviderPlugin,
|
||||
type WebSearchProviderToolDefinition,
|
||||
@ -62,18 +61,14 @@ type GrokSearchResponse = {
|
||||
}>;
|
||||
};
|
||||
|
||||
function resolveGrokConfig(config?: OpenClawConfig, searchConfig?: SearchConfigRecord): GrokConfig {
|
||||
const pluginConfig = resolveProviderWebSearchPluginConfig(config, "xai");
|
||||
if (pluginConfig) {
|
||||
return pluginConfig as GrokConfig;
|
||||
}
|
||||
const grok = (searchConfig as Record<string, unknown> | undefined)?.grok;
|
||||
function resolveGrokConfig(searchConfig?: SearchConfigRecord): GrokConfig {
|
||||
const grok = searchConfig?.grok;
|
||||
return grok && typeof grok === "object" && !Array.isArray(grok) ? (grok as GrokConfig) : {};
|
||||
}
|
||||
|
||||
function resolveGrokApiKey(grok?: GrokConfig): string | undefined {
|
||||
return (
|
||||
readConfiguredSecretString(grok?.apiKey, "plugins.entries.xai.config.webSearch.apiKey") ??
|
||||
readConfiguredSecretString(grok?.apiKey, "tools.web.search.grok.apiKey") ??
|
||||
readProviderEnvValue(["XAI_API_KEY"])
|
||||
);
|
||||
}
|
||||
@ -185,7 +180,6 @@ function createGrokSchema() {
|
||||
}
|
||||
|
||||
function createGrokToolDefinition(
|
||||
config?: OpenClawConfig,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
): WebSearchProviderToolDefinition {
|
||||
return {
|
||||
@ -212,13 +206,13 @@ function createGrokToolDefinition(
|
||||
}
|
||||
}
|
||||
|
||||
const grokConfig = resolveGrokConfig(config, searchConfig);
|
||||
const grokConfig = resolveGrokConfig(searchConfig);
|
||||
const apiKey = resolveGrokApiKey(grokConfig);
|
||||
if (!apiKey) {
|
||||
return {
|
||||
error: "missing_xai_api_key",
|
||||
message:
|
||||
"web_search (grok) needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure plugins.entries.xai.config.webSearch.apiKey.",
|
||||
"web_search (grok) needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure tools.web.search.grok.apiKey.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
@ -302,8 +296,19 @@ export function createGrokWebSearchProvider(): WebSearchProviderPlugin {
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
setProviderWebSearchPluginConfigValue(configTarget, "xai", "apiKey", value);
|
||||
},
|
||||
createTool: (ctx) =>
|
||||
createGrokToolDefinition(ctx.config, ctx.searchConfig as SearchConfigRecord | undefined),
|
||||
createTool: (ctx) => {
|
||||
const pluginConfig = resolveProviderWebSearchPluginConfig(ctx.config, "xai");
|
||||
const searchConfig = {
|
||||
...(ctx.searchConfig as SearchConfigRecord | undefined),
|
||||
grok: {
|
||||
...((ctx.searchConfig as SearchConfigRecord | undefined)?.grok as
|
||||
| Record<string, unknown>
|
||||
| undefined),
|
||||
...(pluginConfig as Record<string, unknown> | undefined),
|
||||
},
|
||||
} as SearchConfigRecord;
|
||||
return createGrokToolDefinition(searchConfig);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,8 @@ function isSourceFile(filePath: string): boolean {
|
||||
|
||||
function isProductionExtensionFile(filePath: string): boolean {
|
||||
return !(
|
||||
filePath.endsWith("/runtime-api.ts") ||
|
||||
filePath.endsWith("\\runtime-api.ts") ||
|
||||
filePath.includes(".test.") ||
|
||||
filePath.includes(".spec.") ||
|
||||
filePath.includes(".fixture.") ||
|
||||
|
||||
@ -1,36 +1,123 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { normalizeResolvedSecretInputString } from "../../config/types.secrets.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import type { PluginWebSearchProviderEntry } from "../../plugins/types.js";
|
||||
import { resolvePluginWebSearchProviders } from "../../plugins/web-search-providers.js";
|
||||
import type { RuntimeWebSearchMetadata } from "../../secrets/runtime-web-tools.types.js";
|
||||
import {
|
||||
__testing as runtimeTesting,
|
||||
resolveWebSearchDefinition,
|
||||
} from "../../web-search/runtime.js";
|
||||
import { normalizeSecretInput } from "../../utils/normalize-secret-input.js";
|
||||
import type { AnyAgentTool } from "./common.js";
|
||||
import { jsonResult } from "./common.js";
|
||||
import { SEARCH_CACHE } from "./web-search-provider-common.js";
|
||||
import {
|
||||
resolveSearchConfig,
|
||||
resolveSearchEnabled,
|
||||
type WebSearchConfig,
|
||||
} from "./web-search-provider-config.js";
|
||||
|
||||
function readProviderEnvValue(envVars: string[]): string | undefined {
|
||||
for (const envVar of envVars) {
|
||||
const value = normalizeSecretInput(process.env[envVar]);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function hasProviderCredential(
|
||||
provider: PluginWebSearchProviderEntry,
|
||||
search: WebSearchConfig | undefined,
|
||||
): boolean {
|
||||
const rawValue = provider.getCredentialValue(search as Record<string, unknown> | undefined);
|
||||
const fromConfig = normalizeSecretInput(
|
||||
normalizeResolvedSecretInputString({
|
||||
value: rawValue,
|
||||
path: provider.credentialPath,
|
||||
}),
|
||||
);
|
||||
return Boolean(fromConfig || readProviderEnvValue(provider.envVars));
|
||||
}
|
||||
|
||||
function resolveSearchProvider(search?: WebSearchConfig): string {
|
||||
const providers = resolvePluginWebSearchProviders({
|
||||
bundledAllowlistCompat: true,
|
||||
});
|
||||
const raw =
|
||||
search && "provider" in search && typeof search.provider === "string"
|
||||
? search.provider.trim().toLowerCase()
|
||||
: "";
|
||||
|
||||
if (raw) {
|
||||
const explicit = providers.find((provider) => provider.id === raw);
|
||||
if (explicit) {
|
||||
return explicit.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!raw) {
|
||||
for (const provider of providers) {
|
||||
if (!hasProviderCredential(provider, search)) {
|
||||
continue;
|
||||
}
|
||||
logVerbose(
|
||||
`web_search: no provider configured, auto-detected "${provider.id}" from available API keys`,
|
||||
);
|
||||
return provider.id;
|
||||
}
|
||||
}
|
||||
|
||||
return providers[0]?.id ?? "";
|
||||
}
|
||||
|
||||
export function createWebSearchTool(options?: {
|
||||
config?: OpenClawConfig;
|
||||
sandboxed?: boolean;
|
||||
runtimeWebSearch?: RuntimeWebSearchMetadata;
|
||||
}): AnyAgentTool | null {
|
||||
const resolved = resolveWebSearchDefinition({
|
||||
config: options?.config,
|
||||
sandboxed: options?.sandboxed,
|
||||
runtimeWebSearch: options?.runtimeWebSearch,
|
||||
});
|
||||
if (!resolved) {
|
||||
const search = resolveSearchConfig(options?.config);
|
||||
if (!resolveSearchEnabled({ search, sandboxed: options?.sandboxed })) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const providers = resolvePluginWebSearchProviders({
|
||||
config: options?.config,
|
||||
bundledAllowlistCompat: true,
|
||||
});
|
||||
if (providers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const providerId =
|
||||
options?.runtimeWebSearch?.selectedProvider ??
|
||||
options?.runtimeWebSearch?.providerConfigured ??
|
||||
resolveSearchProvider(search);
|
||||
const provider =
|
||||
providers.find((entry) => entry.id === providerId) ??
|
||||
providers.find((entry) => entry.id === resolveSearchProvider(search)) ??
|
||||
providers[0];
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const definition = provider.createTool({
|
||||
config: options?.config,
|
||||
searchConfig: search as Record<string, unknown> | undefined,
|
||||
runtimeMetadata: options?.runtimeWebSearch,
|
||||
});
|
||||
if (!definition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
label: "Web Search",
|
||||
name: "web_search",
|
||||
description: resolved.definition.description,
|
||||
parameters: resolved.definition.parameters,
|
||||
execute: async (_toolCallId, args) => jsonResult(await resolved.definition.execute(args)),
|
||||
description: definition.description,
|
||||
parameters: definition.parameters,
|
||||
execute: async (_toolCallId, args) => jsonResult(await definition.execute(args)),
|
||||
};
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
SEARCH_CACHE,
|
||||
...runtimeTesting,
|
||||
resolveSearchProvider,
|
||||
};
|
||||
|
||||
@ -12,7 +12,10 @@ import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import type { SecretInputMode } from "./onboard-types.js";
|
||||
|
||||
export type SearchProvider = string;
|
||||
export type SearchProvider = NonNullable<
|
||||
NonNullable<NonNullable<NonNullable<OpenClawConfig["tools"]>["web"]>["search"]>["provider"]
|
||||
>;
|
||||
type SearchConfig = NonNullable<NonNullable<NonNullable<OpenClawConfig["tools"]>["web"]>["search"]>;
|
||||
|
||||
type SearchProviderEntry = {
|
||||
value: SearchProvider;
|
||||
@ -29,7 +32,7 @@ export const SEARCH_PROVIDER_OPTIONS: readonly SearchProviderEntry[] =
|
||||
resolvePluginWebSearchProviders({
|
||||
bundledAllowlistCompat: true,
|
||||
}).map((provider) => ({
|
||||
value: provider.id,
|
||||
value: provider.id as SearchProvider,
|
||||
label: provider.label,
|
||||
hint: provider.hint,
|
||||
envKeys: provider.envVars,
|
||||
@ -44,14 +47,12 @@ export function hasKeyInEnv(entry: SearchProviderEntry): boolean {
|
||||
}
|
||||
|
||||
function rawKeyValue(config: OpenClawConfig, provider: SearchProvider): unknown {
|
||||
const search = config.tools?.web?.search;
|
||||
const entry = resolvePluginWebSearchProviders({
|
||||
config,
|
||||
bundledAllowlistCompat: true,
|
||||
}).find((candidate) => candidate.id === provider);
|
||||
return (
|
||||
entry?.getConfiguredCredentialValue?.(config) ??
|
||||
entry?.getCredentialValue(config.tools?.web?.search as Record<string, unknown> | undefined)
|
||||
);
|
||||
return entry?.getCredentialValue(search as Record<string, unknown> | undefined);
|
||||
}
|
||||
|
||||
/** Returns the plaintext key string, or undefined for SecretRefs/missing. */
|
||||
@ -101,24 +102,17 @@ export function applySearchKey(
|
||||
config,
|
||||
bundledAllowlistCompat: true,
|
||||
}).find((candidate) => candidate.id === provider);
|
||||
const nextBase = {
|
||||
const search: SearchConfig = { ...config.tools?.web?.search, provider, enabled: true };
|
||||
if (providerEntry) {
|
||||
providerEntry.setCredentialValue(search as Record<string, unknown>, key);
|
||||
}
|
||||
const nextBase: OpenClawConfig = {
|
||||
...config,
|
||||
tools: {
|
||||
...config.tools,
|
||||
web: {
|
||||
...config.tools?.web,
|
||||
search: { ...config.tools?.web?.search, provider, enabled: true },
|
||||
},
|
||||
web: { ...config.tools?.web, search },
|
||||
},
|
||||
};
|
||||
if (providerEntry?.setConfiguredCredentialValue) {
|
||||
providerEntry.setConfiguredCredentialValue(nextBase, key);
|
||||
} else {
|
||||
const search = nextBase.tools?.web?.search as Record<string, unknown> | undefined;
|
||||
if (providerEntry && search) {
|
||||
providerEntry.setCredentialValue(search, key);
|
||||
}
|
||||
}
|
||||
return providerEntry?.applySelectionConfig?.(nextBase) ?? nextBase;
|
||||
}
|
||||
|
||||
@ -127,17 +121,18 @@ function applyProviderOnly(config: OpenClawConfig, provider: SearchProvider): Op
|
||||
config,
|
||||
bundledAllowlistCompat: true,
|
||||
}).find((candidate) => candidate.id === provider);
|
||||
const nextBase = {
|
||||
const search: SearchConfig = {
|
||||
...config.tools?.web?.search,
|
||||
provider,
|
||||
enabled: true,
|
||||
};
|
||||
const nextBase: OpenClawConfig = {
|
||||
...config,
|
||||
tools: {
|
||||
...config.tools,
|
||||
web: {
|
||||
...config.tools?.web,
|
||||
search: {
|
||||
...config.tools?.web?.search,
|
||||
provider,
|
||||
enabled: true,
|
||||
},
|
||||
search,
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -198,7 +193,7 @@ export async function setupSearch(
|
||||
return SEARCH_PROVIDER_OPTIONS[0].value;
|
||||
})();
|
||||
|
||||
type PickerValue = string;
|
||||
type PickerValue = SearchProvider | "__skip__";
|
||||
const choice = await prompter.select<PickerValue>({
|
||||
message: "Search provider",
|
||||
options: [
|
||||
@ -278,16 +273,17 @@ export async function setupSearch(
|
||||
"Web search",
|
||||
);
|
||||
|
||||
const search: SearchConfig = {
|
||||
...config.tools?.web?.search,
|
||||
provider: choice,
|
||||
};
|
||||
return {
|
||||
...config,
|
||||
tools: {
|
||||
...config.tools,
|
||||
web: {
|
||||
...config.tools?.web,
|
||||
search: {
|
||||
...config.tools?.web?.search,
|
||||
provider: choice,
|
||||
},
|
||||
search,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,10 +1,23 @@
|
||||
export type { SignalAccountConfig } from "../config/types.js";
|
||||
export type { ChannelPlugin } from "./channel-plugin-common.js";
|
||||
export {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
buildChannelConfigSchema,
|
||||
deleteAccountFromConfigSection,
|
||||
getChatChannelMeta,
|
||||
setAccountEnabledInConfigSection,
|
||||
} from "./channel-plugin-common.js";
|
||||
export { SignalConfigSchema } from "../config/zod-schema.providers-core.js";
|
||||
export {
|
||||
looksLikeSignalTargetId,
|
||||
normalizeSignalMessagingTarget,
|
||||
} from "../channels/plugins/normalize/signal.js";
|
||||
export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js";
|
||||
export { normalizeE164 } from "../utils.js";
|
||||
export {
|
||||
buildBaseAccountStatusSnapshot,
|
||||
buildBaseChannelStatusSummary,
|
||||
collectStatusIssuesFromLastError,
|
||||
createDefaultChannelRuntimeState,
|
||||
} from "./status-helpers.js";
|
||||
|
||||
@ -52,12 +52,9 @@ export {
|
||||
listSignalAccountIds,
|
||||
resolveDefaultSignalAccountId,
|
||||
} from "../../extensions/signal/api.js";
|
||||
export { resolveSignalReactionLevel } from "../../extensions/signal/src/reaction-level.js";
|
||||
export { signalMessageActions } from "../../extensions/signal/src/message-actions.js";
|
||||
export { monitorSignalProvider } from "../../extensions/signal/src/monitor.js";
|
||||
export { probeSignal } from "../../extensions/signal/src/probe.js";
|
||||
export {
|
||||
removeReactionSignal,
|
||||
sendReactionSignal,
|
||||
} from "../../extensions/signal/src/send-reactions.js";
|
||||
export { sendMessageSignal } from "../../extensions/signal/src/send.js";
|
||||
export { monitorSignalProvider } from "../../extensions/signal/runtime-api.js";
|
||||
export { probeSignal } from "../../extensions/signal/runtime-api.js";
|
||||
export { resolveSignalReactionLevel } from "../../extensions/signal/runtime-api.js";
|
||||
export { removeReactionSignal, sendReactionSignal } from "../../extensions/signal/runtime-api.js";
|
||||
export { sendMessageSignal } from "../../extensions/signal/runtime-api.js";
|
||||
export { signalMessageActions } from "../../extensions/signal/runtime-api.js";
|
||||
|
||||
@ -8,7 +8,6 @@ import {
|
||||
setupAuthTestEnv,
|
||||
} from "../../../test/helpers/auth-wizard.js";
|
||||
import { clearRuntimeAuthProfileStoreSnapshots } from "../../agents/auth-profiles/store.js";
|
||||
import { applyAuthChoiceLoadedPluginProvider } from "../../plugins/provider-auth-choice.js";
|
||||
import { buildProviderPluginMethodChoice } from "../provider-wizard.js";
|
||||
import { requireProviderContractProvider, uniqueProviderContractProviders } from "./registry.js";
|
||||
import { registerProviders, requireProvider } from "./testkit.js";
|
||||
@ -28,23 +27,6 @@ const runProviderModelSelectedHookMock = vi.hoisted(() =>
|
||||
vi.fn<RunProviderModelSelectedHook>(async () => {}),
|
||||
);
|
||||
|
||||
vi.mock("../../../extensions/qwen-portal-auth/oauth.js", () => ({
|
||||
loginQwenPortalOAuth: loginQwenPortalOAuthMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../providers/github-copilot-auth.js", () => ({
|
||||
githubCopilotLoginCommand: githubCopilotLoginCommandMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../plugins/provider-auth-choice.runtime.js", () => ({
|
||||
resolvePluginProviders: resolvePluginProvidersMock,
|
||||
resolveProviderPluginChoice: resolveProviderPluginChoiceMock,
|
||||
runProviderModelSelectedHook: runProviderModelSelectedHookMock,
|
||||
}));
|
||||
|
||||
const { resolvePreferredProviderForAuthChoice } =
|
||||
await import("../../plugins/provider-auth-choice-preference.js");
|
||||
|
||||
type StoredAuthProfile = {
|
||||
type?: string;
|
||||
provider?: string;
|
||||
@ -54,7 +36,9 @@ type StoredAuthProfile = {
|
||||
token?: string;
|
||||
};
|
||||
|
||||
const qwenPortalPlugin = (await import("../../../extensions/qwen-portal-auth/index.js")).default;
|
||||
let applyAuthChoiceLoadedPluginProvider: typeof import("../../plugins/provider-auth-choice.js").applyAuthChoiceLoadedPluginProvider;
|
||||
let resolvePreferredProviderForAuthChoice: typeof import("../../plugins/provider-auth-choice-preference.js").resolvePreferredProviderForAuthChoice;
|
||||
let qwenPortalPlugin: (typeof import("../../../extensions/qwen-portal-auth/index.js"))["default"];
|
||||
|
||||
describe("provider auth-choice contract", () => {
|
||||
const lifecycle = createAuthTestLifecycle([
|
||||
@ -73,7 +57,24 @@ describe("provider auth-choice contract", () => {
|
||||
lifecycle.setStateDir(env.stateDir);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.doMock("../../../extensions/qwen-portal-auth/oauth.js", () => ({
|
||||
loginQwenPortalOAuth: loginQwenPortalOAuthMock,
|
||||
}));
|
||||
vi.doMock("../../providers/github-copilot-auth.js", () => ({
|
||||
githubCopilotLoginCommand: githubCopilotLoginCommandMock,
|
||||
}));
|
||||
vi.doMock("../../plugins/provider-auth-choice.runtime.js", () => ({
|
||||
resolvePluginProviders: resolvePluginProvidersMock,
|
||||
resolveProviderPluginChoice: resolveProviderPluginChoiceMock,
|
||||
runProviderModelSelectedHook: runProviderModelSelectedHookMock,
|
||||
}));
|
||||
({ applyAuthChoiceLoadedPluginProvider } =
|
||||
await import("../../plugins/provider-auth-choice.js"));
|
||||
({ resolvePreferredProviderForAuthChoice } =
|
||||
await import("../../plugins/provider-auth-choice-preference.js"));
|
||||
({ default: qwenPortalPlugin } = await import("../../../extensions/qwen-portal-auth/index.js"));
|
||||
resolvePluginProvidersMock.mockReset();
|
||||
resolvePluginProvidersMock.mockReturnValue(uniqueProviderContractProviders);
|
||||
resolveProviderPluginChoiceMock.mockReset();
|
||||
@ -95,6 +96,7 @@ describe("provider auth-choice contract", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
loginQwenPortalOAuthMock.mockReset();
|
||||
githubCopilotLoginCommandMock.mockReset();
|
||||
resolvePluginProvidersMock.mockReset();
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
clearRuntimeAuthProfileStoreSnapshots,
|
||||
replaceRuntimeAuthProfileStoreSnapshots,
|
||||
} from "../../agents/auth-profiles/store.js";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { clearRuntimeAuthProfileStoreSnapshots } from "../../agents/auth-profiles/store.js";
|
||||
import type { AuthProfileStore } from "../../agents/auth-profiles/types.js";
|
||||
import { createNonExitingRuntime } from "../../runtime.js";
|
||||
import { createCapturedPluginRegistration } from "../../test-utils/plugin-registration.js";
|
||||
import type {
|
||||
@ -14,34 +12,51 @@ import type {
|
||||
import type { OpenClawPluginApi, ProviderPlugin } from "../types.js";
|
||||
|
||||
type LoginOpenAICodexOAuth =
|
||||
(typeof import("openclaw/plugin-sdk/provider-auth-login"))["loginOpenAICodexOAuth"];
|
||||
(typeof import("openclaw/plugin-sdk/provider-auth"))["loginOpenAICodexOAuth"];
|
||||
type LoginQwenPortalOAuth =
|
||||
(typeof import("../../../extensions/qwen-portal-auth/oauth.js"))["loginQwenPortalOAuth"];
|
||||
type GithubCopilotLoginCommand =
|
||||
(typeof import("openclaw/plugin-sdk/provider-auth-login"))["githubCopilotLoginCommand"];
|
||||
(typeof import("openclaw/plugin-sdk/provider-auth"))["githubCopilotLoginCommand"];
|
||||
type CreateVpsAwareHandlers =
|
||||
(typeof import("../provider-oauth-flow.js"))["createVpsAwareOAuthHandlers"];
|
||||
type EnsureAuthProfileStore =
|
||||
typeof import("openclaw/plugin-sdk/agent-runtime").ensureAuthProfileStore;
|
||||
type ListProfilesForProvider =
|
||||
typeof import("openclaw/plugin-sdk/agent-runtime").listProfilesForProvider;
|
||||
|
||||
const loginOpenAICodexOAuthMock = vi.hoisted(() => vi.fn<LoginOpenAICodexOAuth>());
|
||||
const loginQwenPortalOAuthMock = vi.hoisted(() => vi.fn<LoginQwenPortalOAuth>());
|
||||
const githubCopilotLoginCommandMock = vi.hoisted(() => vi.fn<GithubCopilotLoginCommand>());
|
||||
const ensureAuthProfileStoreMock = vi.hoisted(() => vi.fn<EnsureAuthProfileStore>());
|
||||
const listProfilesForProviderMock = vi.hoisted(() => vi.fn<ListProfilesForProvider>());
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/provider-auth-login", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/provider-auth-login")>();
|
||||
vi.mock("openclaw/plugin-sdk/provider-auth", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/provider-auth")>();
|
||||
return {
|
||||
...actual,
|
||||
ensureAuthProfileStore: ensureAuthProfileStoreMock,
|
||||
listProfilesForProvider: listProfilesForProviderMock,
|
||||
loginOpenAICodexOAuth: loginOpenAICodexOAuthMock,
|
||||
githubCopilotLoginCommand: githubCopilotLoginCommandMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/agent-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/agent-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
ensureAuthProfileStore: ensureAuthProfileStoreMock,
|
||||
listProfilesForProvider: listProfilesForProviderMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../../extensions/qwen-portal-auth/oauth.js", () => ({
|
||||
loginQwenPortalOAuth: loginQwenPortalOAuthMock,
|
||||
}));
|
||||
|
||||
const openAIPlugin = (await import("../../../extensions/openai/index.js")).default;
|
||||
const qwenPortalPlugin = (await import("../../../extensions/qwen-portal-auth/index.js")).default;
|
||||
const githubCopilotPlugin = (await import("../../../extensions/github-copilot/index.js")).default;
|
||||
import githubCopilotPlugin from "../../../extensions/github-copilot/index.js";
|
||||
import openAIPlugin from "../../../extensions/openai/index.js";
|
||||
import qwenPortalPlugin from "../../../extensions/qwen-portal-auth/index.js";
|
||||
|
||||
function registerProviders(...plugins: Array<{ register(api: OpenClawPluginApi): void }>) {
|
||||
const captured = createCapturedPluginRegistration();
|
||||
@ -96,10 +111,26 @@ function buildAuthContext() {
|
||||
}
|
||||
|
||||
describe("provider auth contract", () => {
|
||||
let authStore: AuthProfileStore;
|
||||
|
||||
beforeEach(() => {
|
||||
authStore = { version: 1, profiles: {} };
|
||||
ensureAuthProfileStoreMock.mockReset();
|
||||
ensureAuthProfileStoreMock.mockImplementation(() => authStore);
|
||||
listProfilesForProviderMock.mockReset();
|
||||
listProfilesForProviderMock.mockImplementation((store, providerId) =>
|
||||
Object.entries(store.profiles)
|
||||
.filter(([, credential]) => credential?.provider === providerId)
|
||||
.map(([profileId]) => profileId),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
loginOpenAICodexOAuthMock.mockReset();
|
||||
loginQwenPortalOAuthMock.mockReset();
|
||||
githubCopilotLoginCommandMock.mockReset();
|
||||
ensureAuthProfileStoreMock.mockReset();
|
||||
listProfilesForProviderMock.mockReset();
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
});
|
||||
|
||||
@ -197,20 +228,11 @@ describe("provider auth contract", () => {
|
||||
|
||||
it("keeps GitHub Copilot device auth results provider-owned", async () => {
|
||||
const provider = requireProvider(registerProviders(githubCopilotPlugin), "github-copilot");
|
||||
replaceRuntimeAuthProfileStoreSnapshots([
|
||||
{
|
||||
store: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"github-copilot:github": {
|
||||
type: "token",
|
||||
provider: "github-copilot",
|
||||
token: "github-device-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
authStore.profiles["github-copilot:github"] = {
|
||||
type: "token" as const,
|
||||
provider: "github-copilot",
|
||||
token: "github-device-token",
|
||||
};
|
||||
|
||||
const stdin = process.stdin as NodeJS.ReadStream & { isTTY?: boolean };
|
||||
const hadOwnIsTTY = Object.prototype.hasOwnProperty.call(stdin, "isTTY");
|
||||
|
||||
@ -1,11 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
clearRuntimeAuthProfileStoreSnapshots,
|
||||
replaceRuntimeAuthProfileStoreSnapshots,
|
||||
} from "../../agents/auth-profiles/store.js";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { QWEN_OAUTH_MARKER } from "../../agents/model-auth-markers.js";
|
||||
import type { ModelDefinitionConfig } from "../../config/types.models.js";
|
||||
import { runProviderCatalog } from "../provider-discovery.js";
|
||||
import { registerProviders, requireProvider } from "./testkit.js";
|
||||
|
||||
const resolveCopilotApiTokenMock = vi.hoisted(() => vi.fn());
|
||||
@ -13,66 +8,18 @@ const buildOllamaProviderMock = vi.hoisted(() => vi.fn());
|
||||
const buildVllmProviderMock = vi.hoisted(() => vi.fn());
|
||||
const buildSglangProviderMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../../extensions/github-copilot/token.js", async () => {
|
||||
const actual = await vi.importActual<object>("../../../extensions/github-copilot/token.js");
|
||||
return {
|
||||
...actual,
|
||||
resolveCopilotApiToken: resolveCopilotApiTokenMock,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/provider-setup", async () => {
|
||||
const actual = await vi.importActual<object>("openclaw/plugin-sdk/provider-setup");
|
||||
return {
|
||||
...actual,
|
||||
buildOllamaProvider: (...args: unknown[]) => buildOllamaProviderMock(...args),
|
||||
buildVllmProvider: (...args: unknown[]) => buildVllmProviderMock(...args),
|
||||
buildSglangProvider: (...args: unknown[]) => buildSglangProviderMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/self-hosted-provider-setup", async () => {
|
||||
const actual = await vi.importActual<object>("openclaw/plugin-sdk/self-hosted-provider-setup");
|
||||
return {
|
||||
...actual,
|
||||
buildVllmProvider: (...args: unknown[]) => buildVllmProviderMock(...args),
|
||||
buildSglangProvider: (...args: unknown[]) => buildSglangProviderMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/ollama-setup", async () => {
|
||||
const actual = await vi.importActual<object>("openclaw/plugin-sdk/ollama-setup");
|
||||
return {
|
||||
...actual,
|
||||
buildOllamaProvider: (...args: unknown[]) => buildOllamaProviderMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
const qwenPortalPlugin = (await import("../../../extensions/qwen-portal-auth/index.js")).default;
|
||||
const githubCopilotPlugin = (await import("../../../extensions/github-copilot/index.js")).default;
|
||||
const ollamaPlugin = (await import("../../../extensions/ollama/index.js")).default;
|
||||
const vllmPlugin = (await import("../../../extensions/vllm/index.js")).default;
|
||||
const sglangPlugin = (await import("../../../extensions/sglang/index.js")).default;
|
||||
const minimaxPlugin = (await import("../../../extensions/minimax/index.js")).default;
|
||||
const modelStudioPlugin = (await import("../../../extensions/modelstudio/index.js")).default;
|
||||
const cloudflareAiGatewayPlugin = (
|
||||
await import("../../../extensions/cloudflare-ai-gateway/index.js")
|
||||
).default;
|
||||
const qwenPortalProvider = requireProvider(registerProviders(qwenPortalPlugin), "qwen-portal");
|
||||
const githubCopilotProvider = requireProvider(
|
||||
registerProviders(githubCopilotPlugin),
|
||||
"github-copilot",
|
||||
);
|
||||
const ollamaProvider = requireProvider(registerProviders(ollamaPlugin), "ollama");
|
||||
const vllmProvider = requireProvider(registerProviders(vllmPlugin), "vllm");
|
||||
const sglangProvider = requireProvider(registerProviders(sglangPlugin), "sglang");
|
||||
const minimaxProvider = requireProvider(registerProviders(minimaxPlugin), "minimax");
|
||||
const minimaxPortalProvider = requireProvider(registerProviders(minimaxPlugin), "minimax-portal");
|
||||
const modelStudioProvider = requireProvider(registerProviders(modelStudioPlugin), "modelstudio");
|
||||
const cloudflareAiGatewayProvider = requireProvider(
|
||||
registerProviders(cloudflareAiGatewayPlugin),
|
||||
"cloudflare-ai-gateway",
|
||||
);
|
||||
let runProviderCatalog: typeof import("../provider-discovery.js").runProviderCatalog;
|
||||
let qwenPortalProvider: Awaited<ReturnType<typeof requireProvider>>;
|
||||
let githubCopilotProvider: Awaited<ReturnType<typeof requireProvider>>;
|
||||
let ollamaProvider: Awaited<ReturnType<typeof requireProvider>>;
|
||||
let vllmProvider: Awaited<ReturnType<typeof requireProvider>>;
|
||||
let sglangProvider: Awaited<ReturnType<typeof requireProvider>>;
|
||||
let minimaxProvider: Awaited<ReturnType<typeof requireProvider>>;
|
||||
let minimaxPortalProvider: Awaited<ReturnType<typeof requireProvider>>;
|
||||
let modelStudioProvider: Awaited<ReturnType<typeof requireProvider>>;
|
||||
let cloudflareAiGatewayProvider: Awaited<ReturnType<typeof requireProvider>>;
|
||||
let clearRuntimeAuthProfileStoreSnapshots: typeof import("../../agents/auth-profiles/store.js").clearRuntimeAuthProfileStoreSnapshots;
|
||||
let replaceRuntimeAuthProfileStoreSnapshots: typeof import("../../agents/auth-profiles/store.js").replaceRuntimeAuthProfileStoreSnapshots;
|
||||
|
||||
function createModelConfig(id: string, name = id): ModelDefinitionConfig {
|
||||
return {
|
||||
@ -159,7 +106,83 @@ function runCatalog(params: {
|
||||
}
|
||||
|
||||
describe("provider discovery contract", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.doMock("../../../extensions/github-copilot/token.js", async () => {
|
||||
const actual = await vi.importActual<object>("../../../extensions/github-copilot/token.js");
|
||||
return {
|
||||
...actual,
|
||||
resolveCopilotApiToken: resolveCopilotApiTokenMock,
|
||||
};
|
||||
});
|
||||
vi.doMock("openclaw/plugin-sdk/provider-setup", async () => {
|
||||
const actual = await vi.importActual<object>("openclaw/plugin-sdk/provider-setup");
|
||||
return {
|
||||
...actual,
|
||||
buildOllamaProvider: (...args: unknown[]) => buildOllamaProviderMock(...args),
|
||||
buildVllmProvider: (...args: unknown[]) => buildVllmProviderMock(...args),
|
||||
buildSglangProvider: (...args: unknown[]) => buildSglangProviderMock(...args),
|
||||
};
|
||||
});
|
||||
vi.doMock("openclaw/plugin-sdk/self-hosted-provider-setup", async () => {
|
||||
const actual = await vi.importActual<object>(
|
||||
"openclaw/plugin-sdk/self-hosted-provider-setup",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
buildVllmProvider: (...args: unknown[]) => buildVllmProviderMock(...args),
|
||||
buildSglangProvider: (...args: unknown[]) => buildSglangProviderMock(...args),
|
||||
};
|
||||
});
|
||||
vi.doMock("openclaw/plugin-sdk/ollama-setup", async () => {
|
||||
const actual = await vi.importActual<object>("openclaw/plugin-sdk/ollama-setup");
|
||||
return {
|
||||
...actual,
|
||||
buildOllamaProvider: (...args: unknown[]) => buildOllamaProviderMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
({ clearRuntimeAuthProfileStoreSnapshots, replaceRuntimeAuthProfileStoreSnapshots } =
|
||||
await import("../../agents/auth-profiles/store.js"));
|
||||
({ runProviderCatalog } = await import("../provider-discovery.js"));
|
||||
const [
|
||||
{ default: qwenPortalPlugin },
|
||||
{ default: githubCopilotPlugin },
|
||||
{ default: ollamaPlugin },
|
||||
{ default: vllmPlugin },
|
||||
{ default: sglangPlugin },
|
||||
{ default: minimaxPlugin },
|
||||
{ default: modelStudioPlugin },
|
||||
{ default: cloudflareAiGatewayPlugin },
|
||||
] = await Promise.all([
|
||||
import("../../../extensions/qwen-portal-auth/index.js"),
|
||||
import("../../../extensions/github-copilot/index.js"),
|
||||
import("../../../extensions/ollama/index.js"),
|
||||
import("../../../extensions/vllm/index.js"),
|
||||
import("../../../extensions/sglang/index.js"),
|
||||
import("../../../extensions/minimax/index.js"),
|
||||
import("../../../extensions/modelstudio/index.js"),
|
||||
import("../../../extensions/cloudflare-ai-gateway/index.js"),
|
||||
]);
|
||||
qwenPortalProvider = requireProvider(registerProviders(qwenPortalPlugin), "qwen-portal");
|
||||
githubCopilotProvider = requireProvider(
|
||||
registerProviders(githubCopilotPlugin),
|
||||
"github-copilot",
|
||||
);
|
||||
ollamaProvider = requireProvider(registerProviders(ollamaPlugin), "ollama");
|
||||
vllmProvider = requireProvider(registerProviders(vllmPlugin), "vllm");
|
||||
sglangProvider = requireProvider(registerProviders(sglangPlugin), "sglang");
|
||||
minimaxProvider = requireProvider(registerProviders(minimaxPlugin), "minimax");
|
||||
minimaxPortalProvider = requireProvider(registerProviders(minimaxPlugin), "minimax-portal");
|
||||
modelStudioProvider = requireProvider(registerProviders(modelStudioPlugin), "modelstudio");
|
||||
cloudflareAiGatewayProvider = requireProvider(
|
||||
registerProviders(cloudflareAiGatewayPlugin),
|
||||
"cloudflare-ai-gateway",
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
resolveCopilotApiTokenMock.mockReset();
|
||||
buildOllamaProviderMock.mockReset();
|
||||
buildVllmProviderMock.mockReset();
|
||||
|
||||
@ -1,8 +1,43 @@
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { withBundledPluginEnablementCompat } from "../bundled-compat.js";
|
||||
import { resolveBundledWebSearchPluginIds } from "../bundled-web-search.js";
|
||||
import { loadOpenClawPlugins } from "../loader.js";
|
||||
import { createPluginLoaderLogger } from "../logger.js";
|
||||
import amazonBedrockPlugin from "../../../extensions/amazon-bedrock/index.js";
|
||||
import anthropicPlugin from "../../../extensions/anthropic/index.js";
|
||||
import bravePlugin from "../../../extensions/brave/index.js";
|
||||
import byteplusPlugin from "../../../extensions/byteplus/index.js";
|
||||
import chutesPlugin from "../../../extensions/chutes/index.js";
|
||||
import cloudflareAiGatewayPlugin from "../../../extensions/cloudflare-ai-gateway/index.js";
|
||||
import copilotProxyPlugin from "../../../extensions/copilot-proxy/index.js";
|
||||
import elevenLabsPlugin from "../../../extensions/elevenlabs/index.js";
|
||||
import falPlugin from "../../../extensions/fal/index.js";
|
||||
import firecrawlPlugin from "../../../extensions/firecrawl/index.js";
|
||||
import githubCopilotPlugin from "../../../extensions/github-copilot/index.js";
|
||||
import googlePlugin from "../../../extensions/google/index.js";
|
||||
import huggingFacePlugin from "../../../extensions/huggingface/index.js";
|
||||
import kilocodePlugin from "../../../extensions/kilocode/index.js";
|
||||
import kimiCodingPlugin from "../../../extensions/kimi-coding/index.js";
|
||||
import microsoftPlugin from "../../../extensions/microsoft/index.js";
|
||||
import minimaxPlugin from "../../../extensions/minimax/index.js";
|
||||
import mistralPlugin from "../../../extensions/mistral/index.js";
|
||||
import modelStudioPlugin from "../../../extensions/modelstudio/index.js";
|
||||
import moonshotPlugin from "../../../extensions/moonshot/index.js";
|
||||
import nvidiaPlugin from "../../../extensions/nvidia/index.js";
|
||||
import ollamaPlugin from "../../../extensions/ollama/index.js";
|
||||
import openAIPlugin from "../../../extensions/openai/index.js";
|
||||
import opencodeGoPlugin from "../../../extensions/opencode-go/index.js";
|
||||
import opencodePlugin from "../../../extensions/opencode/index.js";
|
||||
import openrouterPlugin from "../../../extensions/openrouter/index.js";
|
||||
import perplexityPlugin from "../../../extensions/perplexity/index.js";
|
||||
import qianfanPlugin from "../../../extensions/qianfan/index.js";
|
||||
import qwenPortalAuthPlugin from "../../../extensions/qwen-portal-auth/index.js";
|
||||
import sglangPlugin from "../../../extensions/sglang/index.js";
|
||||
import syntheticPlugin from "../../../extensions/synthetic/index.js";
|
||||
import togetherPlugin from "../../../extensions/together/index.js";
|
||||
import venicePlugin from "../../../extensions/venice/index.js";
|
||||
import vercelAiGatewayPlugin from "../../../extensions/vercel-ai-gateway/index.js";
|
||||
import vllmPlugin from "../../../extensions/vllm/index.js";
|
||||
import volcenginePlugin from "../../../extensions/volcengine/index.js";
|
||||
import xaiPlugin from "../../../extensions/xai/index.js";
|
||||
import xiaomiPlugin from "../../../extensions/xiaomi/index.js";
|
||||
import zaiPlugin from "../../../extensions/zai/index.js";
|
||||
import { createCapturedPluginRegistration } from "../captured-registration.js";
|
||||
import { resolvePluginProviders } from "../providers.js";
|
||||
import type {
|
||||
ImageGenerationProviderPlugin,
|
||||
@ -12,6 +47,11 @@ import type {
|
||||
WebSearchProviderPlugin,
|
||||
} from "../types.js";
|
||||
|
||||
type RegistrablePlugin = {
|
||||
id: string;
|
||||
register: (api: ReturnType<typeof createCapturedPluginRegistration>["api"]) => void;
|
||||
};
|
||||
|
||||
type CapabilityContractEntry<T> = {
|
||||
pluginId: string;
|
||||
provider: T;
|
||||
@ -38,30 +78,57 @@ type PluginRegistrationContractEntry = {
|
||||
toolNames: string[];
|
||||
};
|
||||
|
||||
const log = createSubsystemLogger("plugins");
|
||||
const bundledWebSearchPlugins: Array<RegistrablePlugin & { credentialValue: unknown }> = [
|
||||
{ ...bravePlugin, credentialValue: "BSA-test" },
|
||||
{ ...firecrawlPlugin, credentialValue: "fc-test" },
|
||||
{ ...googlePlugin, credentialValue: "AIza-test" },
|
||||
{ ...moonshotPlugin, credentialValue: "sk-test" },
|
||||
{ ...perplexityPlugin, credentialValue: "pplx-test" },
|
||||
{ ...xaiPlugin, credentialValue: "xai-test" },
|
||||
];
|
||||
|
||||
const BUNDLED_WEB_SEARCH_CREDENTIAL_VALUES: Readonly<Record<string, unknown>> = {
|
||||
brave: "BSA-test",
|
||||
firecrawl: "fc-test",
|
||||
google: "AIza-test",
|
||||
moonshot: "sk-test",
|
||||
perplexity: "pplx-test",
|
||||
xai: "xai-test",
|
||||
};
|
||||
const bundledSpeechPlugins: RegistrablePlugin[] = [elevenLabsPlugin, microsoftPlugin, openAIPlugin];
|
||||
|
||||
const BUNDLED_SPEECH_PLUGIN_IDS = ["elevenlabs", "microsoft", "openai"] as const;
|
||||
const BUNDLED_MEDIA_UNDERSTANDING_PLUGIN_IDS = [
|
||||
"anthropic",
|
||||
"google",
|
||||
"minimax",
|
||||
"mistral",
|
||||
"moonshot",
|
||||
"openai",
|
||||
"zai",
|
||||
] as const;
|
||||
const BUNDLED_IMAGE_GENERATION_PLUGIN_IDS = ["fal", "google", "openai"] as const;
|
||||
const bundledMediaUnderstandingPlugins: RegistrablePlugin[] = [
|
||||
anthropicPlugin,
|
||||
googlePlugin,
|
||||
minimaxPlugin,
|
||||
mistralPlugin,
|
||||
moonshotPlugin,
|
||||
openAIPlugin,
|
||||
zaiPlugin,
|
||||
];
|
||||
|
||||
export const providerContractRegistry: ProviderContractEntry[] = [];
|
||||
const bundledImageGenerationPlugins: RegistrablePlugin[] = [googlePlugin, openAIPlugin];
|
||||
|
||||
function captureRegistrations(plugin: RegistrablePlugin) {
|
||||
const captured = createCapturedPluginRegistration();
|
||||
plugin.register(captured.api);
|
||||
return captured;
|
||||
}
|
||||
|
||||
function buildCapabilityContractRegistry<T>(params: {
|
||||
plugins: RegistrablePlugin[];
|
||||
select: (captured: ReturnType<typeof createCapturedPluginRegistration>) => T[];
|
||||
}): CapabilityContractEntry<T>[] {
|
||||
return params.plugins.flatMap((plugin) => {
|
||||
const captured = captureRegistrations(plugin);
|
||||
return params.select(captured).map((provider) => ({
|
||||
pluginId: plugin.id,
|
||||
provider,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function dedupePlugins<T extends RegistrablePlugin>(
|
||||
plugins: ReadonlyArray<T | undefined | null>,
|
||||
): T[] {
|
||||
return [
|
||||
...new Map(
|
||||
plugins.filter((plugin): plugin is T => Boolean(plugin)).map((plugin) => [plugin.id, plugin]),
|
||||
).values(),
|
||||
];
|
||||
}
|
||||
|
||||
export let providerContractLoadError: Error | undefined;
|
||||
|
||||
@ -87,78 +154,111 @@ function loadBundledProviderRegistry(): ProviderContractEntry[] {
|
||||
}
|
||||
}
|
||||
|
||||
const loadedBundledProviderRegistry: ProviderContractEntry[] = loadBundledProviderRegistry();
|
||||
|
||||
providerContractRegistry.splice(
|
||||
0,
|
||||
providerContractRegistry.length,
|
||||
...loadedBundledProviderRegistry,
|
||||
);
|
||||
|
||||
export const uniqueProviderContractProviders: ProviderPlugin[] = [
|
||||
...new Map(providerContractRegistry.map((entry) => [entry.provider.id, entry.provider])).values(),
|
||||
];
|
||||
|
||||
export const providerContractPluginIds = [
|
||||
...new Set(providerContractRegistry.map((entry) => entry.pluginId)),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
|
||||
export const providerContractCompatPluginIds = providerContractPluginIds.map((pluginId) =>
|
||||
pluginId === "kimi-coding" ? "kimi" : pluginId,
|
||||
);
|
||||
|
||||
const bundledCapabilityContractPluginIds = [
|
||||
...new Set([
|
||||
...providerContractCompatPluginIds,
|
||||
...resolveBundledWebSearchPluginIds({}),
|
||||
...BUNDLED_SPEECH_PLUGIN_IDS,
|
||||
...BUNDLED_MEDIA_UNDERSTANDING_PLUGIN_IDS,
|
||||
...BUNDLED_IMAGE_GENERATION_PLUGIN_IDS,
|
||||
]),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
|
||||
export let capabilityContractLoadError: Error | undefined;
|
||||
|
||||
function loadBundledCapabilityRegistry() {
|
||||
try {
|
||||
capabilityContractLoadError = undefined;
|
||||
return loadOpenClawPlugins({
|
||||
config: withBundledPluginEnablementCompat({
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: true,
|
||||
allow: bundledCapabilityContractPluginIds,
|
||||
slots: {
|
||||
memory: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
pluginIds: bundledCapabilityContractPluginIds,
|
||||
}),
|
||||
cache: false,
|
||||
activate: false,
|
||||
logger: createPluginLoaderLogger(log),
|
||||
});
|
||||
} catch (error) {
|
||||
capabilityContractLoadError = error instanceof Error ? error : new Error(String(error));
|
||||
return loadOpenClawPlugins({
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
cache: false,
|
||||
activate: false,
|
||||
logger: createPluginLoaderLogger(log),
|
||||
});
|
||||
}
|
||||
function createLazyArrayView<T>(load: () => T[]): T[] {
|
||||
return new Proxy([] as T[], {
|
||||
get(_target, prop) {
|
||||
const actual = load();
|
||||
const value = Reflect.get(actual, prop, actual);
|
||||
return typeof value === "function" ? value.bind(actual) : value;
|
||||
},
|
||||
has(_target, prop) {
|
||||
return Reflect.has(load(), prop);
|
||||
},
|
||||
ownKeys() {
|
||||
return Reflect.ownKeys(load());
|
||||
},
|
||||
getOwnPropertyDescriptor(_target, prop) {
|
||||
const actual = load();
|
||||
const descriptor = Reflect.getOwnPropertyDescriptor(actual, prop);
|
||||
if (descriptor) {
|
||||
return descriptor;
|
||||
}
|
||||
if (Reflect.has(actual, prop)) {
|
||||
return {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
writable: false,
|
||||
value: Reflect.get(actual, prop, actual),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const loadedBundledCapabilityRegistry = loadBundledCapabilityRegistry();
|
||||
let providerContractRegistryCache: ProviderContractEntry[] | null = null;
|
||||
let webSearchProviderContractRegistryCache: WebSearchProviderContractEntry[] | null = null;
|
||||
let speechProviderContractRegistryCache: SpeechProviderContractEntry[] | null = null;
|
||||
let mediaUnderstandingProviderContractRegistryCache:
|
||||
| MediaUnderstandingProviderContractEntry[]
|
||||
| null = null;
|
||||
let imageGenerationProviderContractRegistryCache: ImageGenerationProviderContractEntry[] | null =
|
||||
null;
|
||||
let pluginRegistrationContractRegistryCache: PluginRegistrationContractEntry[] | null = null;
|
||||
let providerRegistrationEntriesLoaded = false;
|
||||
|
||||
function loadProviderContractRegistry(): ProviderContractEntry[] {
|
||||
if (!providerContractRegistryCache) {
|
||||
providerContractRegistryCache = buildCapabilityContractRegistry({
|
||||
plugins: bundledProviderPlugins,
|
||||
select: (captured) => captured.providers,
|
||||
}).map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
}));
|
||||
}
|
||||
if (!providerRegistrationEntriesLoaded) {
|
||||
const registrationEntries = loadPluginRegistrationContractRegistry();
|
||||
if (!providerRegistrationEntriesLoaded) {
|
||||
mergeProviderContractRegistrations(registrationEntries, providerContractRegistryCache);
|
||||
providerRegistrationEntriesLoaded = true;
|
||||
}
|
||||
}
|
||||
return providerContractRegistryCache;
|
||||
}
|
||||
|
||||
function loadUniqueProviderContractProviders(): ProviderPlugin[] {
|
||||
return [
|
||||
...new Map(
|
||||
loadProviderContractRegistry().map((entry) => [entry.provider.id, entry.provider]),
|
||||
).values(),
|
||||
];
|
||||
}
|
||||
|
||||
function loadProviderContractPluginIds(): string[] {
|
||||
return [...new Set(loadProviderContractRegistry().map((entry) => entry.pluginId))].toSorted(
|
||||
(left, right) => left.localeCompare(right),
|
||||
);
|
||||
}
|
||||
|
||||
function loadProviderContractCompatPluginIds(): string[] {
|
||||
return loadProviderContractPluginIds().map((pluginId) =>
|
||||
pluginId === "kimi-coding" ? "kimi" : pluginId,
|
||||
);
|
||||
}
|
||||
|
||||
export const providerContractRegistry: ProviderContractEntry[] = createLazyArrayView(
|
||||
loadProviderContractRegistry,
|
||||
);
|
||||
|
||||
export const uniqueProviderContractProviders: ProviderPlugin[] = createLazyArrayView(
|
||||
loadUniqueProviderContractProviders,
|
||||
);
|
||||
|
||||
export const providerContractPluginIds: string[] = createLazyArrayView(
|
||||
loadProviderContractPluginIds,
|
||||
);
|
||||
|
||||
export const providerContractCompatPluginIds: string[] = createLazyArrayView(
|
||||
loadProviderContractCompatPluginIds,
|
||||
);
|
||||
|
||||
export function requireProviderContractProvider(providerId: string): ProviderPlugin {
|
||||
const provider = uniqueProviderContractProviders.find((entry) => entry.id === providerId);
|
||||
if (!provider) {
|
||||
if (!providerContractLoadError) {
|
||||
loadBundledProviderRegistry();
|
||||
}
|
||||
if (providerContractLoadError) {
|
||||
throw new Error(
|
||||
`provider contract entry missing for ${providerId}; bundled provider registry failed to load: ${providerContractLoadError.message}`,
|
||||
@ -195,51 +295,190 @@ export function resolveProviderContractProvidersForPluginIds(
|
||||
];
|
||||
}
|
||||
|
||||
export const webSearchProviderContractRegistry: WebSearchProviderContractEntry[] =
|
||||
loadedBundledCapabilityRegistry.webSearchProviders
|
||||
.filter((entry) => entry.pluginId in BUNDLED_WEB_SEARCH_CREDENTIAL_VALUES)
|
||||
.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
credentialValue: BUNDLED_WEB_SEARCH_CREDENTIAL_VALUES[entry.pluginId],
|
||||
}));
|
||||
function loadWebSearchProviderContractRegistry(): WebSearchProviderContractEntry[] {
|
||||
if (!webSearchProviderContractRegistryCache) {
|
||||
webSearchProviderContractRegistryCache = bundledWebSearchPlugins.flatMap((plugin) => {
|
||||
const captured = captureRegistrations(plugin);
|
||||
return captured.webSearchProviders.map((provider) => ({
|
||||
pluginId: plugin.id,
|
||||
provider,
|
||||
credentialValue: plugin.credentialValue,
|
||||
}));
|
||||
});
|
||||
}
|
||||
return webSearchProviderContractRegistryCache;
|
||||
}
|
||||
|
||||
export const speechProviderContractRegistry: SpeechProviderContractEntry[] =
|
||||
loadedBundledCapabilityRegistry.speechProviders.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
}));
|
||||
function loadSpeechProviderContractRegistry(): SpeechProviderContractEntry[] {
|
||||
if (!speechProviderContractRegistryCache) {
|
||||
speechProviderContractRegistryCache = buildCapabilityContractRegistry({
|
||||
plugins: bundledSpeechPlugins,
|
||||
select: (captured) => captured.speechProviders,
|
||||
});
|
||||
}
|
||||
return speechProviderContractRegistryCache;
|
||||
}
|
||||
|
||||
function loadMediaUnderstandingProviderContractRegistry(): MediaUnderstandingProviderContractEntry[] {
|
||||
if (!mediaUnderstandingProviderContractRegistryCache) {
|
||||
mediaUnderstandingProviderContractRegistryCache = buildCapabilityContractRegistry({
|
||||
plugins: bundledMediaUnderstandingPlugins,
|
||||
select: (captured) => captured.mediaUnderstandingProviders,
|
||||
});
|
||||
}
|
||||
return mediaUnderstandingProviderContractRegistryCache;
|
||||
}
|
||||
|
||||
function loadImageGenerationProviderContractRegistry(): ImageGenerationProviderContractEntry[] {
|
||||
if (!imageGenerationProviderContractRegistryCache) {
|
||||
imageGenerationProviderContractRegistryCache = buildCapabilityContractRegistry({
|
||||
plugins: bundledImageGenerationPlugins,
|
||||
select: (captured) => captured.imageGenerationProviders,
|
||||
});
|
||||
}
|
||||
return imageGenerationProviderContractRegistryCache;
|
||||
}
|
||||
|
||||
export const webSearchProviderContractRegistry: WebSearchProviderContractEntry[] =
|
||||
createLazyArrayView(loadWebSearchProviderContractRegistry);
|
||||
|
||||
export const speechProviderContractRegistry: SpeechProviderContractEntry[] = createLazyArrayView(
|
||||
loadSpeechProviderContractRegistry,
|
||||
);
|
||||
|
||||
export const mediaUnderstandingProviderContractRegistry: MediaUnderstandingProviderContractEntry[] =
|
||||
loadedBundledCapabilityRegistry.mediaUnderstandingProviders.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
}));
|
||||
createLazyArrayView(loadMediaUnderstandingProviderContractRegistry);
|
||||
|
||||
export const imageGenerationProviderContractRegistry: ImageGenerationProviderContractEntry[] =
|
||||
loadedBundledCapabilityRegistry.imageGenerationProviders.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
}));
|
||||
createLazyArrayView(loadImageGenerationProviderContractRegistry);
|
||||
|
||||
const bundledProviderPlugins = dedupePlugins([
|
||||
amazonBedrockPlugin,
|
||||
anthropicPlugin,
|
||||
byteplusPlugin,
|
||||
chutesPlugin,
|
||||
cloudflareAiGatewayPlugin,
|
||||
copilotProxyPlugin,
|
||||
githubCopilotPlugin,
|
||||
falPlugin,
|
||||
googlePlugin,
|
||||
huggingFacePlugin,
|
||||
kilocodePlugin,
|
||||
kimiCodingPlugin,
|
||||
minimaxPlugin,
|
||||
mistralPlugin,
|
||||
modelStudioPlugin,
|
||||
moonshotPlugin,
|
||||
nvidiaPlugin,
|
||||
ollamaPlugin,
|
||||
openAIPlugin,
|
||||
opencodePlugin,
|
||||
opencodeGoPlugin,
|
||||
openrouterPlugin,
|
||||
qianfanPlugin,
|
||||
qwenPortalAuthPlugin,
|
||||
sglangPlugin,
|
||||
syntheticPlugin,
|
||||
togetherPlugin,
|
||||
venicePlugin,
|
||||
vercelAiGatewayPlugin,
|
||||
vllmPlugin,
|
||||
volcenginePlugin,
|
||||
xaiPlugin,
|
||||
xiaomiPlugin,
|
||||
zaiPlugin,
|
||||
]);
|
||||
|
||||
const bundledPluginRegistrationList = dedupePlugins([
|
||||
...bundledSpeechPlugins,
|
||||
...bundledMediaUnderstandingPlugins,
|
||||
...bundledImageGenerationPlugins,
|
||||
...bundledWebSearchPlugins,
|
||||
]);
|
||||
|
||||
function mergeIds(existing: string[], next: string[]): string[] {
|
||||
return next.length > 0 ? next : existing;
|
||||
}
|
||||
|
||||
function upsertPluginRegistrationContractEntry(
|
||||
entries: PluginRegistrationContractEntry[],
|
||||
next: PluginRegistrationContractEntry,
|
||||
): void {
|
||||
const existing = entries.find((entry) => entry.pluginId === next.pluginId);
|
||||
if (!existing) {
|
||||
entries.push(next);
|
||||
return;
|
||||
}
|
||||
existing.providerIds = mergeIds(existing.providerIds, next.providerIds);
|
||||
existing.speechProviderIds = mergeIds(existing.speechProviderIds, next.speechProviderIds);
|
||||
existing.mediaUnderstandingProviderIds = mergeIds(
|
||||
existing.mediaUnderstandingProviderIds,
|
||||
next.mediaUnderstandingProviderIds,
|
||||
);
|
||||
existing.imageGenerationProviderIds = mergeIds(
|
||||
existing.imageGenerationProviderIds,
|
||||
next.imageGenerationProviderIds,
|
||||
);
|
||||
existing.webSearchProviderIds = mergeIds(
|
||||
existing.webSearchProviderIds,
|
||||
next.webSearchProviderIds,
|
||||
);
|
||||
existing.toolNames = mergeIds(existing.toolNames, next.toolNames);
|
||||
}
|
||||
|
||||
function mergeProviderContractRegistrations(
|
||||
registrationEntries: PluginRegistrationContractEntry[],
|
||||
providerEntries: ProviderContractEntry[],
|
||||
): void {
|
||||
const byPluginId = new Map<string, string[]>();
|
||||
for (const entry of providerEntries) {
|
||||
const providerIds = byPluginId.get(entry.pluginId) ?? [];
|
||||
providerIds.push(entry.provider.id);
|
||||
byPluginId.set(entry.pluginId, providerIds);
|
||||
}
|
||||
for (const [pluginId, providerIds] of byPluginId) {
|
||||
upsertPluginRegistrationContractEntry(registrationEntries, {
|
||||
pluginId,
|
||||
providerIds: providerIds.toSorted((left, right) => left.localeCompare(right)),
|
||||
speechProviderIds: [],
|
||||
mediaUnderstandingProviderIds: [],
|
||||
imageGenerationProviderIds: [],
|
||||
webSearchProviderIds: [],
|
||||
toolNames: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function loadPluginRegistrationContractRegistry(): PluginRegistrationContractEntry[] {
|
||||
if (!pluginRegistrationContractRegistryCache) {
|
||||
const entries: PluginRegistrationContractEntry[] = [];
|
||||
for (const plugin of bundledPluginRegistrationList) {
|
||||
const captured = captureRegistrations(plugin);
|
||||
upsertPluginRegistrationContractEntry(entries, {
|
||||
pluginId: plugin.id,
|
||||
providerIds: captured.providers.map((provider) => provider.id),
|
||||
speechProviderIds: captured.speechProviders.map((provider) => provider.id),
|
||||
mediaUnderstandingProviderIds: captured.mediaUnderstandingProviders.map(
|
||||
(provider) => provider.id,
|
||||
),
|
||||
imageGenerationProviderIds: captured.imageGenerationProviders.map(
|
||||
(provider) => provider.id,
|
||||
),
|
||||
webSearchProviderIds: captured.webSearchProviders.map((provider) => provider.id),
|
||||
toolNames: captured.tools.map((tool) => tool.name),
|
||||
});
|
||||
}
|
||||
pluginRegistrationContractRegistryCache = entries;
|
||||
}
|
||||
if (providerContractRegistryCache && !providerRegistrationEntriesLoaded) {
|
||||
mergeProviderContractRegistrations(
|
||||
pluginRegistrationContractRegistryCache,
|
||||
providerContractRegistryCache,
|
||||
);
|
||||
providerRegistrationEntriesLoaded = true;
|
||||
}
|
||||
return pluginRegistrationContractRegistryCache;
|
||||
}
|
||||
|
||||
export const pluginRegistrationContractRegistry: PluginRegistrationContractEntry[] =
|
||||
loadedBundledCapabilityRegistry.plugins
|
||||
.filter(
|
||||
(plugin) =>
|
||||
plugin.origin === "bundled" &&
|
||||
(plugin.providerIds.length > 0 ||
|
||||
plugin.speechProviderIds.length > 0 ||
|
||||
plugin.mediaUnderstandingProviderIds.length > 0 ||
|
||||
plugin.imageGenerationProviderIds.length > 0 ||
|
||||
plugin.webSearchProviderIds.length > 0 ||
|
||||
plugin.toolNames.length > 0),
|
||||
)
|
||||
.map((plugin) => ({
|
||||
pluginId: plugin.id,
|
||||
providerIds: plugin.providerIds,
|
||||
speechProviderIds: plugin.speechProviderIds,
|
||||
mediaUnderstandingProviderIds: plugin.mediaUnderstandingProviderIds,
|
||||
imageGenerationProviderIds: plugin.imageGenerationProviderIds,
|
||||
webSearchProviderIds: plugin.webSearchProviderIds,
|
||||
toolNames: plugin.toolNames,
|
||||
}));
|
||||
createLazyArrayView(loadPluginRegistrationContractRegistry);
|
||||
|
||||
@ -20,6 +20,7 @@ describe("web search runtime", () => {
|
||||
envVars: ["CUSTOM_SEARCH_API_KEY"],
|
||||
placeholder: "custom-...",
|
||||
signupUrl: "https://example.com/signup",
|
||||
credentialPath: "tools.web.search.custom.apiKey",
|
||||
autoDetectOrder: 1,
|
||||
credentialPath: "tools.web.search.custom.apiKey",
|
||||
getCredentialValue: () => "configured",
|
||||
|
||||
@ -611,8 +611,8 @@
|
||||
"file": "src/plugins/runtime/runtime-whatsapp.ts",
|
||||
"line": 85,
|
||||
"kind": "dynamic-import",
|
||||
"specifier": "../../../extensions/whatsapp/action-runtime.runtime.js",
|
||||
"resolvedPath": "extensions/whatsapp/action-runtime.runtime.js",
|
||||
"specifier": "../../../extensions/whatsapp/action-runtime-api.js",
|
||||
"resolvedPath": "extensions/whatsapp/action-runtime-api.js",
|
||||
"reason": "dynamically imports extension-owned file from src/plugins"
|
||||
}
|
||||
]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user