refactor(web-search): share scoped provider config plumbing

This commit is contained in:
Vincent Koc
2026-03-19 23:50:25 -07:00
parent 96f21c37b4
commit a562fb5550
10 changed files with 136 additions and 183 deletions

View File

@@ -71,6 +71,37 @@ export function setScopedCredentialValue(
(scoped as Record<string, unknown>).apiKey = value;
}
export function mergeScopedSearchConfig(
searchConfig: Record<string, unknown> | undefined,
key: string,
pluginConfig: Record<string, unknown> | undefined,
options?: { mirrorApiKeyToTopLevel?: boolean },
): Record<string, unknown> | undefined {
if (!pluginConfig) {
return searchConfig;
}
const currentScoped =
searchConfig?.[key] &&
typeof searchConfig[key] === "object" &&
!Array.isArray(searchConfig[key])
? (searchConfig[key] as Record<string, unknown>)
: {};
const next: Record<string, unknown> = {
...searchConfig,
[key]: {
...currentScoped,
...pluginConfig,
},
};
if (options?.mirrorApiKeyToTopLevel && pluginConfig.apiKey !== undefined) {
next.apiKey = pluginConfig.apiKey;
}
return next;
}
export function resolveSearchConfig(cfg?: OpenClawConfig): WebSearchConfig {
const search = cfg?.tools?.web?.search;
if (!search || typeof search !== "object") {

View File

@@ -3,7 +3,10 @@ import { __testing as braveTesting } from "../../../extensions/brave/src/brave-w
import { __testing as moonshotTesting } from "../../../extensions/moonshot/src/kimi-web-search-provider.js";
import { __testing as perplexityTesting } from "../../../extensions/perplexity/web-search-provider.js";
import { __testing as xaiTesting } from "../../../extensions/xai/src/grok-web-search-provider.js";
import { buildUnsupportedSearchFilterResponse } from "../../plugin-sdk/provider-web-search.js";
import {
buildUnsupportedSearchFilterResponse,
mergeScopedSearchConfig,
} from "../../plugin-sdk/provider-web-search.js";
import { withEnv } from "../../test-utils/env.js";
const {
inferPerplexityBaseUrlFromApiKey,
@@ -223,6 +226,40 @@ describe("web_search unsupported filter response", () => {
});
});
describe("web_search scoped config merge", () => {
it("returns the original config when no plugin config exists", () => {
const searchConfig = { provider: "grok", grok: { model: "grok-4-1-fast" } };
expect(mergeScopedSearchConfig(searchConfig, "grok", undefined)).toBe(searchConfig);
});
it("merges plugin config into the scoped provider object", () => {
expect(
mergeScopedSearchConfig({ provider: "grok", grok: { model: "old-model" } }, "grok", {
model: "new-model",
apiKey: "xai-test-key",
}),
).toEqual({
provider: "grok",
grok: { model: "new-model", apiKey: "xai-test-key" },
});
});
it("can mirror the plugin apiKey to the top level config", () => {
expect(
mergeScopedSearchConfig(
{ provider: "brave", brave: { count: 5 } },
"brave",
{ apiKey: "brave-test-key" },
{ mirrorApiKeyToTopLevel: true },
),
).toEqual({
provider: "brave",
apiKey: "brave-test-key",
brave: { count: 5, apiKey: "brave-test-key" },
});
});
});
describe("web_search kimi config resolution", () => {
it("uses config apiKey when provided", () => {
expect(resolveKimiApiKey({ apiKey: "kimi-test-key" })).toBe("kimi-test-key");

View File

@@ -30,6 +30,7 @@ export {
export {
getScopedCredentialValue,
getTopLevelCredentialValue,
mergeScopedSearchConfig,
resolveProviderWebSearchPluginConfig,
setScopedCredentialValue,
setProviderWebSearchPluginConfigValue,