mirror of https://github.com/openclaw/openclaw.git
xAI: reuse web search key for provider auth
This commit is contained in:
parent
38e4b77e60
commit
2d919cf63d
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in New Issue