xAI: reuse web search key for provider auth

This commit is contained in:
huntharo 2026-03-27 13:29:05 -04:00 committed by Peter Steinberger
parent 38e4b77e60
commit 2d919cf63d
4 changed files with 107 additions and 1 deletions

View File

@ -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

View File

@ -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

View File

@ -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<string, unknown>,
): { apiKey: string; source: string } | undefined {
const tools = config.tools;
if (!tools || typeof tools !== "object") {
return undefined;
}
const web = (tools as Record<string, unknown>).web;
if (!web || typeof web !== "object") {
return undefined;
}
const search = (web as Record<string, unknown>).search;
if (!search || typeof search !== "object") {
return undefined;
}
const grok = (search as Record<string, unknown>).grok;
if (!grok || typeof grok !== "object") {
return undefined;
}
const apiKey = readConfiguredOrManagedApiKey((grok as Record<string, unknown>).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<string, unknown>;
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 }),

View File

@ -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");