xAI: reuse fallback auth for runtime and discovery

This commit is contained in:
huntharo 2026-03-27 17:39:44 -04:00 committed by Peter Steinberger
parent 800042a3d5
commit 9dd08a49a4
6 changed files with 285 additions and 15 deletions

View File

@ -6,8 +6,9 @@ import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-en
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 { applyXaiModelCompat, normalizeXaiModelId } from "./api.js";
import { applyXaiConfig, XAI_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildXaiProvider } from "./provider-catalog.js";
import { isModernXaiModel, resolveXaiForwardCompatModel } from "./provider-models.js";
import {
createXaiFastModeWrapper,

View File

@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { NON_ENV_SECRETREF_MARKER } from "../../src/agents/model-auth-markers.js";
import { capturePluginRegistration } from "../../src/plugins/captured-registration.js";
import { withEnv } from "../../test/helpers/extensions/env.js";
import xaiPlugin from "./index.js";
@ -158,6 +159,34 @@ describe("xai web search config resolution", () => {
}),
).toEqual({
apiKey: "xai-legacy-fallback",
source: "tools.web.search.grok.apiKey",
mode: "api-key",
});
});
it("returns a managed marker for SecretRef-backed plugin auth fallback", () => {
const captured = capturePluginRegistration(xaiPlugin);
const provider = captured.providers[0];
expect(
provider?.resolveSyntheticAuth?.({
config: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: { source: "file", provider: "vault", id: "/xai/api-key" },
},
},
},
},
},
},
provider: "xai",
providerConfig: undefined,
}),
).toEqual({
apiKey: NON_ENV_SECRETREF_MARKER,
source: "plugins.entries.xai.config.webSearch.apiKey",
mode: "api-key",
});

View File

@ -1,5 +1,6 @@
import { streamSimpleOpenAICompletions, type Model } from "@mariozechner/pi-ai";
import { afterEach, describe, expect, it, vi } from "vitest";
import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../config/config.js";
import type { ModelProviderConfig } from "../config/config.js";
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
import type { AuthProfileStore } from "./auth-profiles.js";
@ -22,8 +23,71 @@ vi.mock("../plugins/provider-runtime.js", () => ({
buildProviderMissingAuthMessageWithPlugin: () => undefined,
resolveProviderSyntheticAuthWithPlugin: (params: {
provider: string;
config?: {
plugins?: {
enabled?: boolean;
entries?: {
xai?: {
enabled?: boolean;
config?: {
webSearch?: {
apiKey?: unknown;
};
};
};
};
};
tools?: {
web?: {
search?: {
grok?: {
apiKey?: unknown;
};
};
};
};
};
context: { providerConfig?: { api?: string; baseUrl?: string; models?: unknown[] } };
}) => {
if (params.provider === "xai") {
if (
params.config?.plugins?.enabled === false ||
params.config?.plugins?.entries?.xai?.enabled === false
) {
return undefined;
}
const pluginApiKey = params.config?.plugins?.entries?.xai?.config?.webSearch?.apiKey;
if (typeof pluginApiKey === "string" && pluginApiKey.trim()) {
return {
apiKey: pluginApiKey.trim(),
source: "plugins.entries.xai.config.webSearch.apiKey",
mode: "api-key" as const,
};
}
if (pluginApiKey && typeof pluginApiKey === "object") {
return {
apiKey: NON_ENV_SECRETREF_MARKER,
source: "plugins.entries.xai.config.webSearch.apiKey",
mode: "api-key" as const,
};
}
const legacyApiKey = params.config?.tools?.web?.search?.grok?.apiKey;
if (typeof legacyApiKey === "string" && legacyApiKey.trim()) {
return {
apiKey: legacyApiKey.trim(),
source: "tools.web.search.grok.apiKey",
mode: "api-key" as const,
};
}
if (legacyApiKey && typeof legacyApiKey === "object") {
return {
apiKey: NON_ENV_SECRETREF_MARKER,
source: "tools.web.search.grok.apiKey",
mode: "api-key" as const,
};
}
return undefined;
}
if (params.provider !== "ollama") {
return undefined;
}
@ -43,6 +107,10 @@ vi.mock("../plugins/provider-runtime.js", () => ({
},
}));
afterEach(() => {
clearRuntimeConfigSnapshot();
});
function createCustomProviderConfig(
baseUrl: string,
modelId = "llama3",
@ -316,6 +384,76 @@ describe("resolveUsableCustomProviderApiKey", () => {
});
});
describe("resolveApiKeyForProvider", () => {
it("reuses the xai plugin web search key without models.providers.xai", async () => {
const resolved = await resolveApiKeyForProvider({
provider: "xai",
cfg: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "xai-plugin-fallback-key", // pragma: allowlist secret
},
},
},
},
},
},
store: { version: 1, profiles: {} },
});
expect(resolved).toMatchObject({
apiKey: "xai-plugin-fallback-key",
source: "plugins.entries.xai.config.webSearch.apiKey",
mode: "api-key",
});
});
it("prefers the active runtime snapshot for SecretRef-backed xai fallback auth", async () => {
const sourceConfig = {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: { source: "file", provider: "vault", id: "/xai/api-key" },
},
},
},
},
},
};
const runtimeConfig = {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "xai-runtime-key", // pragma: allowlist secret
},
},
},
},
},
};
setRuntimeConfigSnapshot(runtimeConfig, sourceConfig);
const resolved = await resolveApiKeyForProvider({
provider: "xai",
cfg: sourceConfig,
store: { version: 1, profiles: {} },
});
expect(resolved).toMatchObject({
apiKey: "xai-runtime-key",
source: "plugins.entries.xai.config.webSearch.apiKey",
mode: "api-key",
});
});
});
describe("resolveApiKeyForProvider synthetic local auth for custom providers", () => {
it("synthesizes a local auth marker for custom providers with a local baseUrl and no apiKey", async () => {
const auth = await resolveCustomProviderAuth(

View File

@ -1,7 +1,7 @@
import path from "node:path";
import { type Api, type Model } from "@mariozechner/pi-ai";
import { formatCliCommand } from "../cli/command-format.js";
import type { OpenClawConfig } from "../config/config.js";
import { getRuntimeConfigSnapshot, type OpenClawConfig } from "../config/config.js";
import type { ModelProviderAuthMode, ModelProviderConfig } from "../config/types.js";
import { coerceSecretRef } from "../config/types.secrets.js";
import { getShellEnvAppliedKeys } from "../infra/shell-env.js";
@ -26,6 +26,7 @@ import {
CUSTOM_LOCAL_AUTH_MARKER,
isKnownEnvApiKeyMarker,
isNonSecretApiKeyMarker,
NON_ENV_SECRETREF_MARKER,
} from "./model-auth-markers.js";
import {
requireApiKey,
@ -189,10 +190,69 @@ function isCustomLocalProviderConfig(providerConfig: ModelProviderConfig): boole
);
}
function isManagedSecretRefApiKeyMarker(apiKey: string | undefined): boolean {
return apiKey?.trim() === NON_ENV_SECRETREF_MARKER;
}
type SyntheticProviderAuthResolution = {
auth?: ResolvedProviderAuth;
blockedOnManagedSecretRef?: boolean;
};
function resolveProviderSyntheticRuntimeAuth(params: {
cfg: OpenClawConfig | undefined;
provider: string;
}): SyntheticProviderAuthResolution {
const resolveFromConfig = (
config: OpenClawConfig | undefined,
): ResolvedProviderAuth | undefined => {
const providerConfig = resolveProviderConfig(config, params.provider);
return resolveProviderSyntheticAuthWithPlugin({
provider: params.provider,
config,
context: {
config,
provider: params.provider,
providerConfig,
},
});
};
const directAuth = resolveFromConfig(params.cfg);
if (!directAuth) {
return {};
}
if (!isManagedSecretRefApiKeyMarker(directAuth.apiKey)) {
return { auth: directAuth };
}
const runtimeConfig = getRuntimeConfigSnapshot();
if (!runtimeConfig || runtimeConfig === params.cfg) {
return { blockedOnManagedSecretRef: true };
}
const runtimeAuth = resolveFromConfig(runtimeConfig);
const runtimeApiKey = runtimeAuth?.apiKey;
if (!runtimeAuth || !runtimeApiKey || isNonSecretApiKeyMarker(runtimeApiKey)) {
return { blockedOnManagedSecretRef: true };
}
return {
auth: runtimeAuth,
};
}
function resolveSyntheticLocalProviderAuth(params: {
cfg: OpenClawConfig | undefined;
provider: string;
}): ResolvedProviderAuth | null {
const syntheticProviderAuth = resolveProviderSyntheticRuntimeAuth(params);
if (syntheticProviderAuth.auth) {
return syntheticProviderAuth.auth;
}
if (syntheticProviderAuth.blockedOnManagedSecretRef) {
return null;
}
const providerConfig = resolveProviderConfig(params.cfg, params.provider);
if (!providerConfig) {
return null;
@ -206,19 +266,6 @@ function resolveSyntheticLocalProviderAuth(params: {
return null;
}
const pluginSyntheticAuth = resolveProviderSyntheticAuthWithPlugin({
provider: params.provider,
config: params.cfg,
context: {
config: params.cfg,
provider: params.provider,
providerConfig,
},
});
if (pluginSyntheticAuth) {
return pluginSyntheticAuth;
}
const authOverride = resolveProviderAuthOverride(params.cfg, params.provider);
if (authOverride && authOverride !== "api-key") {
return null;

View File

@ -138,6 +138,30 @@ describe("provider discovery auth marker guardrails", () => {
expect(providers?.xai?.apiKey).toBe(NON_ENV_SECRETREF_MARKER);
});
it("surfaces xai provider auth from SecretRef-backed plugin web search config", async () => {
const agentDir = await createAgentDirWithAuthProfiles({});
const providers = await resolveImplicitProvidersForTest({
agentDir,
env: {},
config: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: { source: "file", provider: "vault", id: "/xai/apiKey" },
},
},
},
},
},
},
});
expect(providers?.xai?.apiKey).toBe(NON_ENV_SECRETREF_MARKER);
});
it("surfaces xai provider auth from legacy grok web search config without persisting plaintext", async () => {
const agentDir = await createAgentDirWithAuthProfiles({});
@ -159,4 +183,26 @@ describe("provider discovery auth marker guardrails", () => {
expect(providers?.xai?.apiKey).toBe(NON_ENV_SECRETREF_MARKER);
});
it("surfaces xai provider auth from SecretRef-backed legacy grok web search config", async () => {
const agentDir = await createAgentDirWithAuthProfiles({});
const providers = await resolveImplicitProvidersForTest({
agentDir,
env: {},
config: {
tools: {
web: {
search: {
grok: {
apiKey: { source: "exec", provider: "vault", id: "providers/xai/token" },
},
},
},
},
},
});
expect(providers?.xai?.apiKey).toBe(NON_ENV_SECRETREF_MARKER);
});
});

View File

@ -1132,6 +1132,15 @@ export type ProviderPlugin = {
/**
* Provider-owned synthetic auth marker.
*
* This hook is the canonical seam for provider-specific fallback auth
* derived from plugin/private config. It may return:
* - a runnable literal credential for runtime callers
* - a non-secret marker for managed-secret source config, which is still useful
* for discovery/bootstrap callers
*
* Runtime callers must not treat non-secret markers as runnable credentials;
* they should retry against the active runtime snapshot when available.
*
* Use this when the provider can operate without a real secret for certain
* configured local/self-hosted cases and wants auth resolution to treat that
* config as available.