From 2d919cf63d482e33d1202132f084cf9c3c7b3d1b Mon Sep 17 00:00:00 2001 From: huntharo Date: Fri, 27 Mar 2026 13:29:05 -0400 Subject: [PATCH] xAI: reuse web search key for provider auth --- docs/providers/xai.md | 2 + docs/tools/grok-search.md | 4 +- extensions/xai/index.ts | 72 +++++++++++++++++++++++++++++++ extensions/xai/web-search.test.ts | 30 +++++++++++++ 4 files changed, 107 insertions(+), 1 deletion(-) diff --git a/docs/providers/xai.md b/docs/providers/xai.md index c57104f081a..72bf91f7fdd 100644 --- a/docs/providers/xai.md +++ b/docs/providers/xai.md @@ -29,6 +29,8 @@ openclaw onboard --auth-choice xai-api-key OpenClaw now uses the xAI Responses API as the bundled xAI transport. The same `XAI_API_KEY` can also power Grok-backed `web_search` and first-class `x_search`. +If you store an xAI key under `plugins.entries.xai.config.webSearch.apiKey`, +the bundled xAI model provider now reuses that key as a fallback too. ## Current bundled model catalog diff --git a/docs/tools/grok-search.md b/docs/tools/grok-search.md index f175504f63d..3c6f78e2ca1 100644 --- a/docs/tools/grok-search.md +++ b/docs/tools/grok-search.md @@ -13,7 +13,9 @@ responses to produce AI-synthesized answers backed by live search results with citations. The same `XAI_API_KEY` can also power the built-in `x_search` tool for X -(formerly Twitter) post search. +(formerly Twitter) post search. If you store the key under +`plugins.entries.xai.config.webSearch.apiKey`, OpenClaw now reuses it as a +fallback for the bundled xAI model provider too. ## Get an API key diff --git a/extensions/xai/index.ts b/extensions/xai/index.ts index dabf35704e8..094ad898224 100644 --- a/extensions/xai/index.ts +++ b/extensions/xai/index.ts @@ -1,5 +1,11 @@ +import { + coerceSecretRef, + resolveNonEnvSecretRefApiKeyMarker, +} from "openclaw/plugin-sdk/provider-auth"; import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; import { createToolStreamWrapper } from "openclaw/plugin-sdk/provider-stream"; +import { resolveProviderWebSearchPluginConfig } from "openclaw/plugin-sdk/provider-web-search"; +import { normalizeSecretInputString } from "openclaw/plugin-sdk/secret-input"; import { applyXaiModelCompat, buildXaiProvider, normalizeXaiModelId } from "./api.js"; import { applyXaiConfig, XAI_DEFAULT_MODEL_REF } from "./onboard.js"; import { isModernXaiModel, resolveXaiForwardCompatModel } from "./provider-models.js"; @@ -12,6 +18,57 @@ import { createXaiWebSearchProvider } from "./web-search.js"; const PROVIDER_ID = "xai"; +function readConfiguredOrManagedApiKey(value: unknown): string | undefined { + const literal = normalizeSecretInputString(value); + if (literal) { + return literal; + } + const ref = coerceSecretRef(value); + return ref ? resolveNonEnvSecretRefApiKeyMarker(ref.source) : undefined; +} + +function readLegacyGrokFallback( + config: Record, +): { apiKey: string; source: string } | undefined { + const tools = config.tools; + if (!tools || typeof tools !== "object") { + return undefined; + } + const web = (tools as Record).web; + if (!web || typeof web !== "object") { + return undefined; + } + const search = (web as Record).search; + if (!search || typeof search !== "object") { + return undefined; + } + const grok = (search as Record).grok; + if (!grok || typeof grok !== "object") { + return undefined; + } + const apiKey = readConfiguredOrManagedApiKey((grok as Record).apiKey); + return apiKey ? { apiKey, source: "tools.web.search.grok.apiKey" } : undefined; +} + +function resolveXaiProviderFallbackAuth( + config: unknown, +): { apiKey: string; source: string } | undefined { + if (!config || typeof config !== "object") { + return undefined; + } + const record = config as Record; + const pluginApiKey = readConfiguredOrManagedApiKey( + resolveProviderWebSearchPluginConfig(record, PROVIDER_ID)?.apiKey, + ); + if (pluginApiKey) { + return { + apiKey: pluginApiKey, + source: "plugins.entries.xai.config.webSearch.apiKey", + }; + } + return readLegacyGrokFallback(record); +} + export default defineSingleProviderPluginEntry({ id: "xai", name: "xAI Plugin", @@ -56,6 +113,21 @@ export default defineSingleProviderPluginEntry({ streamFn = createXaiToolCallArgumentDecodingWrapper(streamFn); return createToolStreamWrapper(streamFn, ctx.extraParams?.tool_stream !== false); }, + // Provider-specific fallback auth stays owned by the xAI plugin so core + // auth/discovery code can consume it generically without parsing xAI's + // private config layout. Callers may receive a real key from the active + // runtime snapshot or a non-secret SecretRef marker from source config. + resolveSyntheticAuth: ({ config }) => { + const fallbackAuth = resolveXaiProviderFallbackAuth(config); + if (!fallbackAuth) { + return undefined; + } + return { + apiKey: fallbackAuth.apiKey, + source: fallbackAuth.source, + mode: "api-key" as const, + }; + }, normalizeResolvedModel: ({ model }) => applyXaiModelCompat(model), normalizeModelId: ({ modelId }) => normalizeXaiModelId(modelId), resolveDynamicModel: (ctx) => resolveXaiForwardCompatModel({ providerId: PROVIDER_ID, ctx }), diff --git a/extensions/xai/web-search.test.ts b/extensions/xai/web-search.test.ts index bf7de2f38e5..4e422df7214 100644 --- a/extensions/xai/web-search.test.ts +++ b/extensions/xai/web-search.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from "vitest"; +import { capturePluginRegistration } from "../../src/plugins/captured-registration.js"; import { withEnv } from "../../test/helpers/extensions/env.js"; +import xaiPlugin from "./index.js"; import { resolveXaiCatalogEntry } from "./model-definitions.js"; import { isModernXaiModel, resolveXaiForwardCompatModel } from "./provider-models.js"; import { __testing, createXaiWebSearchProvider } from "./web-search.js"; @@ -107,6 +109,34 @@ describe("xai web search config resolution", () => { }); }); + it("reuses the plugin web search api key for provider auth fallback", () => { + const captured = capturePluginRegistration(xaiPlugin); + const provider = captured.providers[0]; + expect( + provider?.resolveSyntheticAuth?.({ + config: { + plugins: { + entries: { + xai: { + config: { + webSearch: { + apiKey: "xai-provider-fallback", // pragma: allowlist secret + }, + }, + }, + }, + }, + }, + provider: "xai", + providerConfig: undefined, + }), + ).toEqual({ + apiKey: "xai-provider-fallback", + source: "plugins.entries.xai.config.webSearch.apiKey", + mode: "api-key", + }); + }); + it("uses default model when not specified", () => { expect(resolveXaiWebSearchModel({})).toBe("grok-4-1-fast"); expect(resolveXaiWebSearchModel(undefined)).toBe("grok-4-1-fast");