refactor(providers): share google and xai provider helpers (#60722)

* refactor(google): share oauth token helpers

* refactor(xai): share tool auth fallback helpers

* refactor(xai): share tool auth resolution

* refactor(xai): share tool config helpers

* refactor(xai): share fallback auth helpers

* refactor(xai): share responses tool helpers

* refactor(google): share http request config helper

* fix(xai): re-export shared web search extractor

* fix(xai): import plugin config type

* fix(providers): preserve default google network guard
This commit is contained in:
Vincent Koc 2026-04-04 16:14:15 +09:00 committed by GitHub
parent c87903a4c6
commit 65842aabad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 717 additions and 319 deletions

View File

@ -2,6 +2,8 @@ import { describe, expect, it } from "vitest";
import {
isGoogleGenerativeAiApi,
normalizeGoogleGenerativeAiBaseUrl,
parseGeminiAuth,
resolveGoogleGenerativeAiHttpRequestConfig,
resolveGoogleGenerativeAiApiOrigin,
resolveGoogleGenerativeAiTransport,
shouldNormalizeGoogleGenerativeAiProviderConfig,
@ -91,4 +93,53 @@ describe("google generative ai helpers", () => {
resolveGoogleGenerativeAiApiOrigin("https://generativelanguage.googleapis.com/v1beta"),
).toBe("https://generativelanguage.googleapis.com");
});
it("parses project-aware oauth auth payloads into bearer headers", () => {
expect(parseGeminiAuth(JSON.stringify({ token: "oauth-token", projectId: "project-1" }))).toEqual({
headers: {
Authorization: "Bearer oauth-token",
"Content-Type": "application/json",
},
});
});
it("falls back to API key headers for raw tokens", () => {
expect(parseGeminiAuth("api-key-123")).toEqual({
headers: {
"x-goog-api-key": "api-key-123",
"Content-Type": "application/json",
},
});
});
it("builds shared Google Generative AI HTTP request config", () => {
const oauthConfig = resolveGoogleGenerativeAiHttpRequestConfig({
apiKey: JSON.stringify({ token: "oauth-token" }),
baseUrl: "https://generativelanguage.googleapis.com",
capability: "audio",
transport: "media-understanding",
});
expect(oauthConfig).toMatchObject({
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
allowPrivateNetwork: true,
});
expect(Object.fromEntries(new Headers(oauthConfig.headers).entries())).toEqual({
authorization: "Bearer oauth-token",
"content-type": "application/json",
});
const apiKeyConfig = resolveGoogleGenerativeAiHttpRequestConfig({
apiKey: "api-key-123",
capability: "image",
transport: "http",
});
expect(apiKeyConfig).toMatchObject({
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
allowPrivateNetwork: false,
});
expect(Object.fromEntries(new Headers(apiKeyConfig.headers).entries())).toEqual({
"content-type": "application/json",
"x-goog-api-key": "api-key-123",
});
});
});

View File

@ -1,10 +1,15 @@
import { resolveProviderEndpoint } from "openclaw/plugin-sdk/provider-http";
import {
resolveProviderEndpoint,
resolveProviderHttpRequestConfig,
type ProviderRequestTransportOverrides,
} from "openclaw/plugin-sdk/provider-http";
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import {
applyAgentDefaultModelPrimary,
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
import { normalizeAntigravityModelId, normalizeGoogleModelId } from "./model-id.js";
import { parseGoogleOauthApiKey } from "./oauth-token-shared.js";
export { normalizeAntigravityModelId, normalizeGoogleModelId };
type GoogleApiCarrier = {
@ -138,20 +143,14 @@ export function normalizeGoogleProviderConfig(
}
export function parseGeminiAuth(apiKey: string): { headers: Record<string, string> } {
if (apiKey.startsWith("{")) {
try {
const parsed = JSON.parse(apiKey) as { token?: string; projectId?: string };
if (typeof parsed.token === "string" && parsed.token) {
return {
headers: {
Authorization: `Bearer ${parsed.token}`,
"Content-Type": "application/json",
},
};
}
} catch {
// Fall back to API key mode.
}
const parsed = apiKey.startsWith("{") ? parseGoogleOauthApiKey(apiKey) : null;
if (parsed?.token) {
return {
headers: {
Authorization: `Bearer ${parsed.token}`,
"Content-Type": "application/json",
},
};
}
return {
@ -162,6 +161,28 @@ export function parseGeminiAuth(apiKey: string): { headers: Record<string, strin
};
}
export function resolveGoogleGenerativeAiHttpRequestConfig(params: {
apiKey: string;
baseUrl?: string;
headers?: Record<string, string>;
request?: ProviderRequestTransportOverrides;
capability: "image" | "audio" | "video";
transport: "http" | "media-understanding";
}) {
return resolveProviderHttpRequestConfig({
baseUrl: normalizeGoogleApiBaseUrl(params.baseUrl ?? DEFAULT_GOOGLE_API_BASE_URL),
defaultBaseUrl: DEFAULT_GOOGLE_API_BASE_URL,
allowPrivateNetwork: Boolean(params.baseUrl?.trim()),
headers: params.headers,
request: params.request,
defaultHeaders: parseGeminiAuth(params.apiKey).headers,
provider: "google",
api: "google-generative-ai",
capability: params.capability,
transport: params.transport,
});
}
export const GOOGLE_GEMINI_DEFAULT_MODEL = "google/gemini-3.1-pro-preview";
export function applyGoogleGeminiModelDefault(cfg: OpenClawConfig): {

View File

@ -5,6 +5,7 @@ import type {
} from "openclaw/plugin-sdk/plugin-entry";
import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-auth-result";
import { fetchGeminiUsage } from "openclaw/plugin-sdk/provider-usage";
import { formatGoogleOauthApiKey, parseGoogleUsageToken } from "./oauth-token-shared.js";
import { isModernGoogleModel, resolveGoogle31ForwardCompatModel } from "./provider-models.js";
import { buildGoogleGeminiProviderHooks } from "./replay-policy.js";
@ -22,32 +23,6 @@ const GOOGLE_GEMINI_CLI_PROVIDER_HOOKS = buildGoogleGeminiProviderHooks({
includeToolSchemaCompat: true,
});
function parseGoogleUsageToken(apiKey: string): string {
try {
const parsed = JSON.parse(apiKey) as { token?: unknown };
if (typeof parsed?.token === "string") {
return parsed.token;
}
} catch {
// ignore
}
return apiKey;
}
function formatGoogleOauthApiKey(cred: {
type?: string;
access?: string;
projectId?: string;
}): string {
if (cred.type !== "oauth" || typeof cred.access !== "string" || !cred.access.trim()) {
return "";
}
return JSON.stringify({
token: cred.access,
projectId: cred.projectId,
});
}
async function fetchGeminiCliUsage(ctx: ProviderFetchUsageSnapshotContext) {
return await fetchGeminiUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn, PROVIDER_ID);
}

View File

@ -3,13 +3,10 @@ import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runt
import {
assertOkOrThrowHttpError,
postJsonRequest,
resolveProviderHttpRequestConfig,
} from "openclaw/plugin-sdk/provider-http";
import {
DEFAULT_GOOGLE_API_BASE_URL,
normalizeGoogleApiBaseUrl,
normalizeGoogleModelId,
parseGeminiAuth,
resolveGoogleGenerativeAiHttpRequestConfig,
} from "./api.js";
const DEFAULT_GOOGLE_IMAGE_MODEL = "gemini-3.1-flash-image-preview";
@ -52,10 +49,6 @@ type GoogleGenerateImageResponse = {
}>;
};
function resolveGoogleBaseUrl(cfg: Parameters<typeof resolveApiKeyForProvider>[0]["cfg"]): string {
return normalizeGoogleApiBaseUrl(cfg?.models?.providers?.google?.baseUrl);
}
function normalizeGoogleImageModel(model: string | undefined): string {
const trimmed = model?.trim();
return normalizeGoogleModelId(trimmed || DEFAULT_GOOGLE_IMAGE_MODEL);
@ -135,13 +128,9 @@ export function buildGoogleImageGenerationProvider(): ImageGenerationProvider {
const model = normalizeGoogleImageModel(req.model);
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
baseUrl: resolveGoogleBaseUrl(req.cfg),
defaultBaseUrl: DEFAULT_GOOGLE_API_BASE_URL,
allowPrivateNetwork: Boolean(req.cfg?.models?.providers?.google?.baseUrl?.trim()),
defaultHeaders: parseGeminiAuth(auth.apiKey).headers,
provider: "google",
api: "google-generative-ai",
resolveGoogleGenerativeAiHttpRequestConfig({
apiKey: auth.apiKey,
baseUrl: req.cfg?.models?.providers?.google?.baseUrl,
capability: "image",
transport: "http",
});

View File

@ -16,6 +16,7 @@ import {
normalizeGoogleModelId,
} from "./api.js";
import { buildGoogleGeminiCliBackend } from "./cli-backend.js";
import { formatGoogleOauthApiKey } from "./oauth-token-shared.js";
import { isModernGoogleModel, resolveGoogle31ForwardCompatModel } from "./provider-models.js";
import { buildGoogleGeminiProviderHooks } from "./replay-policy.js";
import { createGeminiWebSearchProvider } from "./src/gemini-web-search-provider.js";
@ -30,12 +31,6 @@ const GOOGLE_GEMINI_CLI_ENV_VARS = [
"GEMINI_CLI_OAUTH_CLIENT_SECRET",
] as const;
type GoogleOauthApiKeyCredential = {
type?: string;
access?: string;
projectId?: string;
};
let googleGeminiCliProviderPromise: Promise<ProviderPlugin> | null = null;
let googleImageGenerationProviderPromise: Promise<ImageGenerationProvider> | null = null;
let googleMediaUnderstandingProviderPromise: Promise<MediaUnderstandingProvider> | null = null;
@ -52,16 +47,6 @@ const GOOGLE_GEMINI_PROVIDER_HOOKS_WITH_TOOL_COMPAT = buildGoogleGeminiProviderH
includeToolSchemaCompat: true,
});
function formatGoogleOauthApiKey(cred: GoogleOauthApiKeyCredential): string {
if (cred.type !== "oauth" || typeof cred.access !== "string" || !cred.access.trim()) {
return "";
}
return JSON.stringify({
token: cred.access,
projectId: cred.projectId,
});
}
async function loadGoogleGeminiCliProvider(): Promise<ProviderPlugin> {
if (!googleGeminiCliProviderPromise) {
googleGeminiCliProviderPromise = import("./gemini-cli-provider.js").then((mod) => {
@ -147,7 +132,7 @@ function createLazyGoogleGeminiCliProvider(): ProviderPlugin {
resolveGoogle31ForwardCompatModel({ providerId: GOOGLE_GEMINI_CLI_PROVIDER_ID, ctx }),
...GOOGLE_GEMINI_PROVIDER_HOOKS_WITH_TOOL_COMPAT,
isModernModelRef: ({ modelId }) => isModernGoogleModel(modelId),
formatApiKey: (cred) => formatGoogleOauthApiKey(cred as GoogleOauthApiKeyCredential),
formatApiKey: (cred) => formatGoogleOauthApiKey(cred),
resolveUsageAuth: async (ctx) => {
const provider = await loadGoogleGeminiCliProvider();
return await provider.resolveUsageAuth?.(ctx);

View File

@ -10,14 +10,12 @@ import {
import {
assertOkOrThrowHttpError,
postJsonRequest,
resolveProviderHttpRequestConfig,
type ProviderRequestTransportOverrides,
} from "openclaw/plugin-sdk/provider-http";
import {
DEFAULT_GOOGLE_API_BASE_URL,
normalizeGoogleApiBaseUrl,
normalizeGoogleModelId,
parseGeminiAuth,
resolveGoogleGenerativeAiHttpRequestConfig,
} from "./runtime-api.js";
export const DEFAULT_GOOGLE_AUDIO_BASE_URL = DEFAULT_GOOGLE_API_BASE_URL;
@ -54,19 +52,16 @@ async function generateGeminiInlineDataText(params: {
return normalizeGoogleModelId(trimmed);
})();
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
baseUrl: normalizeGoogleApiBaseUrl(params.baseUrl ?? params.defaultBaseUrl),
defaultBaseUrl: DEFAULT_GOOGLE_API_BASE_URL,
allowPrivateNetwork: Boolean(params.baseUrl?.trim()),
resolveGoogleGenerativeAiHttpRequestConfig({
apiKey: params.apiKey,
baseUrl: params.baseUrl,
headers: params.headers,
request: params.request,
defaultHeaders: parseGeminiAuth(params.apiKey).headers,
provider: "google",
api: "google-generative-ai",
capability: params.defaultMime.startsWith("audio/") ? "audio" : "video",
transport: "media-understanding",
});
const url = `${baseUrl}/models/${model}:generateContent`;
const resolvedBaseUrl = baseUrl ?? params.defaultBaseUrl;
const url = `${resolvedBaseUrl}/models/${model}:generateContent`;
const prompt = (() => {
const trimmed = params.prompt?.trim();

View File

@ -62,6 +62,29 @@ describe("describeGeminiVideo", () => {
expect(result.text).toBe("video ok");
});
it("keeps private-network disabled for the default Google media endpoint", async () => {
const fetchFn = withFetchPreconnect(async () => {
return new Response(
JSON.stringify({
candidates: [{ content: { parts: [{ text: "video ok" }] } }],
}),
{ status: 200, headers: { "content-type": "application/json" } },
);
});
await describeGeminiVideo({
buffer: Buffer.from("video"),
fileName: "clip.mp4",
apiKey: "test-key",
timeoutMs: 1000,
fetchFn,
});
expect(resolvePinnedHostnameWithPolicySpy).toHaveBeenCalled();
const [, options] = resolvePinnedHostnameWithPolicySpy.mock.calls[0] ?? [];
expect(options?.policy?.allowPrivateNetwork).toBeUndefined();
});
it("builds the expected request payload", async () => {
const { fetchFn, getRequest } = createRequestCaptureJsonFetch({
candidates: [

View File

@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import {
formatGoogleOauthApiKey,
parseGoogleOauthApiKey,
parseGoogleUsageToken,
} from "./oauth-token-shared.js";
describe("google oauth token helpers", () => {
it("formats oauth credentials with project-aware payloads", () => {
expect(
formatGoogleOauthApiKey({
type: "oauth",
access: "token-123",
projectId: "project-abc",
}),
).toBe(JSON.stringify({ token: "token-123", projectId: "project-abc" }));
});
it("returns an empty string for non-oauth credentials", () => {
expect(formatGoogleOauthApiKey({ type: "token", access: "token-123" })).toBe("");
});
it("parses project-aware oauth payloads for usage auth", () => {
expect(parseGoogleUsageToken(JSON.stringify({ token: "usage-token" }))).toBe("usage-token");
});
it("parses structured oauth payload fields", () => {
expect(
parseGoogleOauthApiKey(JSON.stringify({ token: "usage-token", projectId: "proj-1" })),
).toEqual({
token: "usage-token",
projectId: "proj-1",
});
});
it("falls back to the raw token when the payload is not JSON", () => {
expect(parseGoogleUsageToken("raw-token")).toBe("raw-token");
});
});

View File

@ -0,0 +1,40 @@
type GoogleOauthApiKeyCredential = {
type?: string;
access?: string;
projectId?: string;
};
export function parseGoogleOauthApiKey(apiKey: string): {
token?: string;
projectId?: string;
} | null {
try {
const parsed = JSON.parse(apiKey) as { token?: unknown; projectId?: unknown };
return {
token: typeof parsed.token === "string" ? parsed.token : undefined,
projectId: typeof parsed.projectId === "string" ? parsed.projectId : undefined,
};
} catch {
return null;
}
}
export function formatGoogleOauthApiKey(cred: GoogleOauthApiKeyCredential): string {
if (cred.type !== "oauth" || typeof cred.access !== "string" || !cred.access.trim()) {
return "";
}
return JSON.stringify({
token: cred.access,
projectId: cred.projectId,
});
}
export function parseGoogleUsageToken(apiKey: string): string {
const parsed = parseGoogleOauthApiKey(apiKey);
if (parsed?.token) {
return parsed.token;
}
// Keep the raw token when the stored credential is not a project-aware JSON payload.
return apiKey;
}

View File

@ -3,4 +3,5 @@ export {
normalizeGoogleApiBaseUrl,
normalizeGoogleModelId,
parseGeminiAuth,
resolveGoogleGenerativeAiHttpRequestConfig,
} from "./api.js";

View File

@ -3,10 +3,7 @@ import { getRuntimeConfigSnapshot } from "openclaw/plugin-sdk/config-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
import {
jsonResult,
readConfiguredSecretString,
readProviderEnvValue,
readStringParam,
resolveProviderWebSearchPluginConfig,
} from "openclaw/plugin-sdk/provider-web-search";
import {
buildXaiCodeExecutionPayload,
@ -14,6 +11,7 @@ import {
resolveXaiCodeExecutionMaxTurns,
resolveXaiCodeExecutionModel,
} from "./src/code-execution-shared.js";
import { isXaiToolEnabled, resolveXaiToolApiKey } from "./src/tool-auth-shared.js";
type XaiPluginConfig = NonNullable<
NonNullable<OpenClawConfig["plugins"]>["entries"]
@ -36,18 +34,6 @@ function readCodeExecutionConfigRecord(
return config && typeof config === "object" ? (config as Record<string, unknown>) : undefined;
}
function readLegacyGrokApiKey(cfg?: OpenClawConfig): string | undefined {
const search = cfg?.tools?.web?.search;
if (!search || typeof search !== "object") {
return undefined;
}
const grok = (search as Record<string, unknown>).grok;
return readConfiguredSecretString(
grok && typeof grok === "object" ? (grok as Record<string, unknown>).apiKey : undefined,
"tools.web.search.grok.apiKey",
);
}
function readPluginCodeExecutionConfig(cfg?: OpenClawConfig): CodeExecutionConfig | undefined {
const entries = cfg?.plugins?.entries;
if (!entries || typeof entries !== "object") {
@ -68,29 +54,16 @@ function readPluginCodeExecutionConfig(cfg?: OpenClawConfig): CodeExecutionConfi
return codeExecution as CodeExecutionConfig;
}
function resolveFallbackXaiApiKey(cfg?: OpenClawConfig): string | undefined {
return (
readConfiguredSecretString(
resolveProviderWebSearchPluginConfig(cfg as Record<string, unknown> | undefined, "xai")
?.apiKey,
"plugins.entries.xai.config.webSearch.apiKey",
) ?? readLegacyGrokApiKey(cfg)
);
}
function resolveCodeExecutionEnabled(params: {
sourceConfig?: OpenClawConfig;
runtimeConfig?: OpenClawConfig;
config?: CodeExecutionConfig;
}): boolean {
if (readCodeExecutionConfigRecord(params.config)?.enabled === false) {
return false;
}
return Boolean(
resolveFallbackXaiApiKey(params.runtimeConfig) ??
resolveFallbackXaiApiKey(params.sourceConfig) ??
readProviderEnvValue(["XAI_API_KEY"]),
);
return isXaiToolEnabled({
enabled: readCodeExecutionConfigRecord(params.config)?.enabled as boolean | undefined,
runtimeConfig: params.runtimeConfig,
sourceConfig: params.sourceConfig,
});
}
export function createCodeExecutionTool(options?: {
@ -123,10 +96,10 @@ export function createCodeExecutionTool(options?: {
}),
}),
execute: async (_toolCallId: string, args: Record<string, unknown>) => {
const apiKey =
resolveFallbackXaiApiKey(runtimeConfig ?? undefined) ??
resolveFallbackXaiApiKey(options?.config) ??
readProviderEnvValue(["XAI_API_KEY"]);
const apiKey = resolveXaiToolApiKey({
runtimeConfig: runtimeConfig ?? undefined,
sourceConfig: options?.config,
});
if (!apiKey) {
return jsonResult({
error: "missing_xai_api_key",

View File

@ -1,8 +1,5 @@
import { Type } from "@sinclair/typebox";
import {
coerceSecretRef,
resolveNonEnvSecretRefApiKeyMarker,
} from "openclaw/plugin-sdk/provider-auth";
import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { buildOpenAICompatibleReplayPolicy } from "openclaw/plugin-sdk/provider-model-shared";
import {
@ -12,9 +9,7 @@ import {
import {
jsonResult,
readProviderEnvValue,
resolveProviderWebSearchPluginConfig,
} from "openclaw/plugin-sdk/provider-web-search";
import { normalizeSecretInputString } from "openclaw/plugin-sdk/secret-input";
import {
applyXaiModelCompat,
normalizeXaiModelId,
@ -31,64 +26,15 @@ import {
createXaiToolCallArgumentDecodingWrapper,
createXaiToolPayloadCompatibilityWrapper,
} from "./stream.js";
import { resolveFallbackXaiAuth } from "./src/tool-auth-shared.js";
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);
}
function hasResolvableXaiApiKey(config: unknown): boolean {
return Boolean(
resolveXaiProviderFallbackAuth(config)?.apiKey || readProviderEnvValue(["XAI_API_KEY"]),
resolveFallbackXaiAuth(config as OpenClawConfig | undefined)?.apiKey ||
readProviderEnvValue(["XAI_API_KEY"]),
);
}
@ -283,7 +229,7 @@ export default defineSingleProviderPluginEntry({
// 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);
const fallbackAuth = resolveFallbackXaiAuth(config as OpenClawConfig | undefined);
if (!fallbackAuth) {
return undefined;
}

View File

@ -1,8 +1,17 @@
import { postTrustedWebToolsJson } from "openclaw/plugin-sdk/provider-web-search";
import { normalizeXaiModelId } from "../model-id.js";
import { extractXaiWebSearchContent, type XaiWebSearchResponse } from "./web-search-shared.js";
import {
buildXaiResponsesToolBody,
resolveXaiResponseTextAndCitations,
XAI_RESPONSES_ENDPOINT,
} from "./responses-tool-shared.js";
import {
coerceXaiToolConfig,
resolveNormalizedXaiToolModel,
resolvePositiveIntegerToolConfig,
} from "./tool-config-shared.js";
import { type XaiWebSearchResponse } from "./web-search-shared.js";
export const XAI_CODE_EXECUTION_ENDPOINT = "https://api.x.ai/v1/responses";
export const XAI_CODE_EXECUTION_ENDPOINT = XAI_RESPONSES_ENDPOINT;
export const XAI_DEFAULT_CODE_EXECUTION_MODEL = "grok-4-1-fast";
export type XaiCodeExecutionConfig = {
@ -24,32 +33,23 @@ export type XaiCodeExecutionResult = {
outputTypes: string[];
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function resolveXaiCodeExecutionConfig(
config?: Record<string, unknown>,
): XaiCodeExecutionConfig {
return isRecord(config) ? (config as XaiCodeExecutionConfig) : {};
return coerceXaiToolConfig<XaiCodeExecutionConfig>(config);
}
export function resolveXaiCodeExecutionModel(config?: Record<string, unknown>): string {
const resolved = resolveXaiCodeExecutionConfig(config);
return typeof resolved.model === "string" && resolved.model.trim()
? normalizeXaiModelId(resolved.model.trim())
: XAI_DEFAULT_CODE_EXECUTION_MODEL;
return resolveNormalizedXaiToolModel({
config,
defaultModel: XAI_DEFAULT_CODE_EXECUTION_MODEL,
});
}
export function resolveXaiCodeExecutionMaxTurns(
config?: Record<string, unknown>,
): number | undefined {
const raw = resolveXaiCodeExecutionConfig(config).maxTurns;
if (typeof raw !== "number" || !Number.isFinite(raw)) {
return undefined;
}
const normalized = Math.trunc(raw);
return normalized > 0 ? normalized : undefined;
return resolvePositiveIntegerToolConfig(config, "maxTurns");
}
export function buildXaiCodeExecutionPayload(params: {
@ -85,17 +85,17 @@ export async function requestXaiCodeExecution(params: {
url: XAI_CODE_EXECUTION_ENDPOINT,
timeoutSeconds: params.timeoutSeconds,
apiKey: params.apiKey,
body: {
body: buildXaiResponsesToolBody({
model: params.model,
input: [{ role: "user", content: params.task }],
inputText: params.task,
tools: [{ type: "code_interpreter" }],
...(params.maxTurns ? { max_turns: params.maxTurns } : {}),
},
maxTurns: params.maxTurns,
}),
errorLabel: "xAI",
},
async (response) => {
const data = (await response.json()) as XaiCodeExecutionResponse;
const { text, annotationCitations } = extractXaiWebSearchContent(data);
const { content, citations } = resolveXaiResponseTextAndCitations(data);
const outputTypes = Array.isArray(data.output)
? [
...new Set(
@ -105,12 +105,8 @@ export async function requestXaiCodeExecution(params: {
),
]
: [];
const citations =
Array.isArray(data.citations) && data.citations.length > 0
? data.citations
: annotationCitations;
return {
content: text ?? "No response",
content,
citations,
usedCodeExecution: outputTypes.includes("code_interpreter_call"),
outputTypes,

View File

@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import { __testing } from "./responses-tool-shared.js";
describe("xai responses tool helpers", () => {
it("builds the shared xAI Responses tool body", () => {
expect(
__testing.buildXaiResponsesToolBody({
model: "grok-4-1-fast",
inputText: "search for openclaw",
tools: [{ type: "x_search" }],
maxTurns: 2,
}),
).toEqual({
model: "grok-4-1-fast",
input: [{ role: "user", content: "search for openclaw" }],
tools: [{ type: "x_search" }],
max_turns: 2,
});
});
it("falls back to annotation citations when the API omits top-level citations", () => {
expect(
__testing.resolveXaiResponseTextAndCitations({
output: [
{
type: "message",
content: [
{
type: "output_text",
text: "Found it",
annotations: [{ type: "url_citation", url: "https://example.com/a" }],
},
],
},
],
}),
).toEqual({
content: "Found it",
citations: ["https://example.com/a"],
});
});
it("prefers explicit top-level citations when present", () => {
expect(
__testing.resolveXaiResponseTextAndCitations({
output_text: "Done",
citations: ["https://example.com/b"],
}),
).toEqual({
content: "Done",
citations: ["https://example.com/b"],
});
});
});

View File

@ -0,0 +1,73 @@
import type { XaiWebSearchResponse } from "./web-search-shared.js";
export const XAI_RESPONSES_ENDPOINT = "https://api.x.ai/v1/responses";
export function buildXaiResponsesToolBody(params: {
model: string;
inputText: string;
tools: Array<Record<string, unknown>>;
maxTurns?: number;
}): Record<string, unknown> {
return {
model: params.model,
input: [{ role: "user", content: params.inputText }],
tools: params.tools,
...(params.maxTurns ? { max_turns: params.maxTurns } : {}),
};
}
export function extractXaiWebSearchContent(data: XaiWebSearchResponse): {
text: string | undefined;
annotationCitations: string[];
} {
for (const output of data.output ?? []) {
if (output.type === "message") {
for (const block of output.content ?? []) {
if (block.type === "output_text" && typeof block.text === "string" && block.text) {
const urls = (block.annotations ?? [])
.filter(
(annotation) =>
annotation.type === "url_citation" && typeof annotation.url === "string",
)
.map((annotation) => annotation.url as string);
return { text: block.text, annotationCitations: [...new Set(urls)] };
}
}
}
if (output.type === "output_text" && typeof output.text === "string" && output.text) {
const urls = (output.annotations ?? [])
.filter(
(annotation) => annotation.type === "url_citation" && typeof annotation.url === "string",
)
.map((annotation) => annotation.url as string);
return { text: output.text, annotationCitations: [...new Set(urls)] };
}
}
return {
text: typeof data.output_text === "string" ? data.output_text : undefined,
annotationCitations: [],
};
}
export function resolveXaiResponseTextAndCitations(data: XaiWebSearchResponse): {
content: string;
citations: string[];
} {
const { text, annotationCitations } = extractXaiWebSearchContent(data);
return {
content: text ?? "No response",
citations:
Array.isArray(data.citations) && data.citations.length > 0
? data.citations
: annotationCitations,
};
}
export const __testing = {
buildXaiResponsesToolBody,
extractXaiWebSearchContent,
resolveXaiResponseTextAndCitations,
XAI_RESPONSES_ENDPOINT,
} as const;

View File

@ -0,0 +1,140 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { NON_ENV_SECRETREF_MARKER } from "openclaw/plugin-sdk/provider-auth-runtime";
import {
isXaiToolEnabled,
resolveFallbackXaiAuth,
resolveFallbackXaiApiKey,
resolveXaiToolApiKey,
} from "./tool-auth-shared.js";
describe("xai tool auth helpers", () => {
afterEach(() => {
vi.unstubAllEnvs();
});
it("prefers plugin web search keys over legacy grok keys", () => {
expect(
resolveFallbackXaiApiKey({
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "plugin-key", // pragma: allowlist secret
},
},
},
},
},
tools: {
web: {
search: {
grok: {
apiKey: "legacy-key", // pragma: allowlist secret
},
},
},
},
}),
).toBe("plugin-key");
});
it("returns source metadata and managed markers for fallback auth", () => {
expect(
resolveFallbackXaiAuth({
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: { source: "file", provider: "vault", id: "/xai/tool-key" },
},
},
},
},
},
}),
).toEqual({
apiKey: NON_ENV_SECRETREF_MARKER,
source: "plugins.entries.xai.config.webSearch.apiKey",
});
expect(
resolveFallbackXaiAuth({
tools: {
web: {
search: {
grok: {
apiKey: "legacy-key", // pragma: allowlist secret
},
},
},
},
}),
).toEqual({
apiKey: "legacy-key",
source: "tools.web.search.grok.apiKey",
});
});
it("falls back to runtime, then source config, then env for tool auth", () => {
vi.stubEnv("XAI_API_KEY", "env-key");
expect(
resolveXaiToolApiKey({
runtimeConfig: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "runtime-key", // pragma: allowlist secret
},
},
},
},
},
},
sourceConfig: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "source-key", // pragma: allowlist secret
},
},
},
},
},
},
}),
).toBe("runtime-key");
expect(
resolveXaiToolApiKey({
sourceConfig: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "source-key", // pragma: allowlist secret
},
},
},
},
},
},
}),
).toBe("source-key");
expect(resolveXaiToolApiKey({})).toBe("env-key");
});
it("honors explicit disabled flags before auth fallback", () => {
vi.stubEnv("XAI_API_KEY", "env-key");
expect(isXaiToolEnabled({ enabled: false })).toBe(false);
expect(isXaiToolEnabled({ enabled: true })).toBe(true);
});
});

View File

@ -0,0 +1,95 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
import {
coerceSecretRef,
resolveNonEnvSecretRefApiKeyMarker,
} from "openclaw/plugin-sdk/provider-auth";
import {
readProviderEnvValue,
readConfiguredSecretString,
resolveProviderWebSearchPluginConfig,
} from "openclaw/plugin-sdk/provider-web-search";
import { normalizeSecretInputString } from "openclaw/plugin-sdk/secret-input";
export type XaiFallbackAuth = {
apiKey: string;
source: string;
};
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 readLegacyGrokFallbackAuth(cfg?: OpenClawConfig): XaiFallbackAuth | undefined {
const search = cfg?.tools?.web?.search;
if (!search || typeof search !== "object") {
return undefined;
}
const grok = (search as Record<string, unknown>).grok;
const apiKey = readConfiguredOrManagedApiKey(
grok && typeof grok === "object" ? (grok as Record<string, unknown>).apiKey : undefined,
);
return apiKey ? { apiKey, source: "tools.web.search.grok.apiKey" } : undefined;
}
export function readLegacyGrokApiKey(cfg?: OpenClawConfig): string | undefined {
const search = cfg?.tools?.web?.search;
if (!search || typeof search !== "object") {
return undefined;
}
const grok = (search as Record<string, unknown>).grok;
return readConfiguredSecretString(
grok && typeof grok === "object" ? (grok as Record<string, unknown>).apiKey : undefined,
"tools.web.search.grok.apiKey",
);
}
export function readPluginXaiWebSearchApiKey(cfg?: OpenClawConfig): string | undefined {
return readConfiguredSecretString(
resolveProviderWebSearchPluginConfig(cfg as Record<string, unknown> | undefined, "xai")?.apiKey,
"plugins.entries.xai.config.webSearch.apiKey",
);
}
export function resolveFallbackXaiAuth(cfg?: OpenClawConfig): XaiFallbackAuth | undefined {
const pluginApiKey = readConfiguredOrManagedApiKey(
resolveProviderWebSearchPluginConfig(cfg as Record<string, unknown> | undefined, "xai")?.apiKey,
);
if (pluginApiKey) {
return {
apiKey: pluginApiKey,
source: "plugins.entries.xai.config.webSearch.apiKey",
};
}
return readLegacyGrokFallbackAuth(cfg);
}
export function resolveFallbackXaiApiKey(cfg?: OpenClawConfig): string | undefined {
return readPluginXaiWebSearchApiKey(cfg) ?? readLegacyGrokApiKey(cfg);
}
export function resolveXaiToolApiKey(params: {
runtimeConfig?: OpenClawConfig;
sourceConfig?: OpenClawConfig;
}): string | undefined {
return (
resolveFallbackXaiApiKey(params.runtimeConfig) ??
resolveFallbackXaiApiKey(params.sourceConfig) ??
readProviderEnvValue(["XAI_API_KEY"])
);
}
export function isXaiToolEnabled(params: {
enabled?: boolean;
runtimeConfig?: OpenClawConfig;
sourceConfig?: OpenClawConfig;
}): boolean {
if (params.enabled === false) {
return false;
}
return Boolean(resolveXaiToolApiKey(params));
}

View File

@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import {
coerceXaiToolConfig,
resolveNormalizedXaiToolModel,
resolvePositiveIntegerToolConfig,
} from "./tool-config-shared.js";
describe("xai tool config helpers", () => {
it("coerces non-record config to an empty object", () => {
expect(coerceXaiToolConfig(undefined)).toEqual({});
expect(coerceXaiToolConfig([] as unknown as Record<string, unknown>)).toEqual({});
});
it("normalizes configured model ids and falls back to the default model", () => {
expect(
resolveNormalizedXaiToolModel({
config: { model: " grok-4.1-fast " },
defaultModel: "grok-4-1-fast",
}),
).toBe("grok-4.1-fast");
expect(
resolveNormalizedXaiToolModel({
config: {},
defaultModel: "grok-4-1-fast",
}),
).toBe("grok-4-1-fast");
});
it("accepts only positive finite numeric turn counts", () => {
expect(resolvePositiveIntegerToolConfig({ maxTurns: 2.9 }, "maxTurns")).toBe(2);
expect(resolvePositiveIntegerToolConfig({ maxTurns: 0 }, "maxTurns")).toBeUndefined();
expect(resolvePositiveIntegerToolConfig({ maxTurns: Number.NaN }, "maxTurns")).toBeUndefined();
expect(resolvePositiveIntegerToolConfig(undefined, "maxTurns")).toBeUndefined();
});
});

View File

@ -0,0 +1,33 @@
import { normalizeXaiModelId } from "../model-id.js";
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function coerceXaiToolConfig<TConfig extends Record<string, unknown>>(
config: Record<string, unknown> | undefined,
): TConfig {
return isRecord(config) ? (config as TConfig) : ({} as TConfig);
}
export function resolveNormalizedXaiToolModel(params: {
config?: Record<string, unknown>;
defaultModel: string;
}): string {
const value = coerceXaiToolConfig<{ model?: unknown }>(params.config).model;
return typeof value === "string" && value.trim()
? normalizeXaiModelId(value.trim())
: params.defaultModel;
}
export function resolvePositiveIntegerToolConfig(
config: Record<string, unknown> | undefined,
key: string,
): number | undefined {
const raw = coerceXaiToolConfig<Record<string, unknown>>(config)[key];
if (typeof raw !== "number" || !Number.isFinite(raw)) {
return undefined;
}
const normalized = Math.trunc(raw);
return normalized > 0 ? normalized : undefined;
}

View File

@ -1,7 +1,14 @@
import { postTrustedWebToolsJson, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search";
import { normalizeXaiModelId } from "../model-id.js";
import {
buildXaiResponsesToolBody,
extractXaiWebSearchContent,
resolveXaiResponseTextAndCitations,
XAI_RESPONSES_ENDPOINT,
} from "./responses-tool-shared.js";
export { extractXaiWebSearchContent } from "./responses-tool-shared.js";
export const XAI_WEB_SEARCH_ENDPOINT = "https://api.x.ai/v1/responses";
export const XAI_WEB_SEARCH_ENDPOINT = XAI_RESPONSES_ENDPOINT;
export const XAI_DEFAULT_WEB_SEARCH_MODEL = "grok-4-1-fast";
export type XaiWebSearchResponse = {
@ -88,41 +95,6 @@ export function resolveXaiInlineCitations(searchConfig?: Record<string, unknown>
return resolveXaiSearchConfig(searchConfig).inlineCitations === true;
}
export function extractXaiWebSearchContent(data: XaiWebSearchResponse): {
text: string | undefined;
annotationCitations: string[];
} {
for (const output of data.output ?? []) {
if (output.type === "message") {
for (const block of output.content ?? []) {
if (block.type === "output_text" && typeof block.text === "string" && block.text) {
const urls = (block.annotations ?? [])
.filter(
(annotation) =>
annotation.type === "url_citation" && typeof annotation.url === "string",
)
.map((annotation) => annotation.url as string);
return { text: block.text, annotationCitations: [...new Set(urls)] };
}
}
}
if (output.type === "output_text" && typeof output.text === "string" && output.text) {
const urls = (output.annotations ?? [])
.filter(
(annotation) => annotation.type === "url_citation" && typeof annotation.url === "string",
)
.map((annotation) => annotation.url as string);
return { text: output.text, annotationCitations: [...new Set(urls)] };
}
}
return {
text: typeof data.output_text === "string" ? data.output_text : undefined,
annotationCitations: [],
};
}
export async function requestXaiWebSearch(params: {
query: string;
model: string;
@ -135,22 +107,18 @@ export async function requestXaiWebSearch(params: {
url: XAI_WEB_SEARCH_ENDPOINT,
timeoutSeconds: params.timeoutSeconds,
apiKey: params.apiKey,
body: {
body: buildXaiResponsesToolBody({
model: params.model,
input: [{ role: "user", content: params.query }],
inputText: params.query,
tools: [{ type: "web_search" }],
},
}),
errorLabel: "xAI",
},
async (response) => {
const data = (await response.json()) as XaiWebSearchResponse;
const { text, annotationCitations } = extractXaiWebSearchContent(data);
const citations =
Array.isArray(data.citations) && data.citations.length > 0
? data.citations
: annotationCitations;
const { content, citations } = resolveXaiResponseTextAndCitations(data);
return {
content: text ?? "No response",
content,
citations,
inlineCitations:
params.inlineCitations && Array.isArray(data.inline_citations)

View File

@ -1,8 +1,17 @@
import { postTrustedWebToolsJson, wrapWebContent } from "openclaw/plugin-sdk/provider-web-search";
import { normalizeXaiModelId } from "../model-id.js";
import { extractXaiWebSearchContent, type XaiWebSearchResponse } from "./web-search-shared.js";
import {
buildXaiResponsesToolBody,
resolveXaiResponseTextAndCitations,
XAI_RESPONSES_ENDPOINT,
} from "./responses-tool-shared.js";
import {
coerceXaiToolConfig,
resolveNormalizedXaiToolModel,
resolvePositiveIntegerToolConfig,
} from "./tool-config-shared.js";
import { type XaiWebSearchResponse } from "./web-search-shared.js";
export const XAI_X_SEARCH_ENDPOINT = "https://api.x.ai/v1/responses";
export const XAI_X_SEARCH_ENDPOINT = XAI_RESPONSES_ENDPOINT;
export const XAI_DEFAULT_X_SEARCH_MODEL = "grok-4-1-fast-non-reasoning";
export type XaiXSearchConfig = {
@ -28,19 +37,15 @@ export type XaiXSearchResult = {
inlineCitations?: XaiWebSearchResponse["inline_citations"];
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function resolveXaiXSearchConfig(config?: Record<string, unknown>): XaiXSearchConfig {
return isRecord(config) ? (config as XaiXSearchConfig) : {};
return coerceXaiToolConfig<XaiXSearchConfig>(config);
}
export function resolveXaiXSearchModel(config?: Record<string, unknown>): string {
const resolved = resolveXaiXSearchConfig(config);
return typeof resolved.model === "string" && resolved.model.trim()
? normalizeXaiModelId(resolved.model.trim())
: XAI_DEFAULT_X_SEARCH_MODEL;
return resolveNormalizedXaiToolModel({
config,
defaultModel: XAI_DEFAULT_X_SEARCH_MODEL,
});
}
export function resolveXaiXSearchInlineCitations(config?: Record<string, unknown>): boolean {
@ -48,12 +53,7 @@ export function resolveXaiXSearchInlineCitations(config?: Record<string, unknown
}
export function resolveXaiXSearchMaxTurns(config?: Record<string, unknown>): number | undefined {
const raw = resolveXaiXSearchConfig(config).maxTurns;
if (typeof raw !== "number" || !Number.isFinite(raw)) {
return undefined;
}
const normalized = Math.trunc(raw);
return normalized > 0 ? normalized : undefined;
return resolvePositiveIntegerToolConfig(config, "maxTurns");
}
function buildXSearchTool(options: XaiXSearchOptions): Record<string, unknown> {
@ -117,23 +117,19 @@ export async function requestXaiXSearch(params: {
url: XAI_X_SEARCH_ENDPOINT,
timeoutSeconds: params.timeoutSeconds,
apiKey: params.apiKey,
body: {
body: buildXaiResponsesToolBody({
model: params.model,
input: [{ role: "user", content: params.options.query }],
inputText: params.options.query,
tools: [buildXSearchTool(params.options)],
...(params.maxTurns ? { max_turns: params.maxTurns } : {}),
},
maxTurns: params.maxTurns,
}),
errorLabel: "xAI",
},
async (response) => {
const data = (await response.json()) as XaiWebSearchResponse;
const { text, annotationCitations } = extractXaiWebSearchContent(data);
const citations =
Array.isArray(data.citations) && data.citations.length > 0
? data.citations
: annotationCitations;
const { content, citations } = resolveXaiResponseTextAndCitations(data);
return {
content: text ?? "No response",
content,
citations,
inlineCitations:
params.inlineCitations && Array.isArray(data.inline_citations)

View File

@ -4,12 +4,9 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
import {
jsonResult,
readCache,
readConfiguredSecretString,
readProviderEnvValue,
readStringArrayParam,
readStringParam,
resolveCacheTtlMs,
resolveProviderWebSearchPluginConfig,
resolveTimeoutSeconds,
writeCache,
} from "openclaw/plugin-sdk/provider-web-search";
@ -17,6 +14,7 @@ import {
resolveEffectiveXSearchConfig,
resolveLegacyXSearchConfig,
} from "./src/x-search-config.js";
import { isXaiToolEnabled, resolveXaiToolApiKey } from "./src/tool-auth-shared.js";
import {
buildXaiXSearchPayload,
requestXaiXSearch,
@ -54,29 +52,6 @@ function getSharedXSearchCache(): Map<string, XSearchCacheEntry> {
const X_SEARCH_CACHE = getSharedXSearchCache();
function readLegacyGrokApiKey(cfg?: OpenClawConfig): string | undefined {
const search = cfg?.tools?.web?.search;
if (!search || typeof search !== "object") {
return undefined;
}
const grok = (search as Record<string, unknown>).grok;
return readConfiguredSecretString(
grok && typeof grok === "object" ? (grok as Record<string, unknown>).apiKey : undefined,
"tools.web.search.grok.apiKey",
);
}
function readPluginXaiWebSearchApiKey(cfg?: OpenClawConfig): string | undefined {
return readConfiguredSecretString(
resolveProviderWebSearchPluginConfig(cfg as Record<string, unknown> | undefined, "xai")?.apiKey,
"plugins.entries.xai.config.webSearch.apiKey",
);
}
function resolveFallbackXaiApiKey(cfg?: OpenClawConfig): string | undefined {
return readPluginXaiWebSearchApiKey(cfg) ?? readLegacyGrokApiKey(cfg);
}
function resolveXSearchConfig(cfg?: OpenClawConfig): Record<string, unknown> | undefined {
return resolveEffectiveXSearchConfig(cfg);
}
@ -86,24 +61,18 @@ function resolveXSearchEnabled(params: {
config?: Record<string, unknown>;
runtimeConfig?: OpenClawConfig;
}): boolean {
if (params.config?.enabled === false) {
return false;
}
if (resolveFallbackXaiApiKey(params.runtimeConfig)) {
return true;
}
return Boolean(resolveFallbackXaiApiKey(params.cfg) || readProviderEnvValue(["XAI_API_KEY"]));
return isXaiToolEnabled({
enabled: params.config?.enabled as boolean | undefined,
runtimeConfig: params.runtimeConfig,
sourceConfig: params.cfg,
});
}
function resolveXSearchApiKey(params: {
sourceConfig?: OpenClawConfig;
runtimeConfig?: OpenClawConfig;
}): string | undefined {
return (
resolveFallbackXaiApiKey(params.runtimeConfig) ??
resolveFallbackXaiApiKey(params.sourceConfig) ??
readProviderEnvValue(["XAI_API_KEY"])
);
return resolveXaiToolApiKey(params);
}
function normalizeOptionalIsoDate(value: string | undefined, label: string): string | undefined {