mirror of https://github.com/openclaw/openclaw.git
xAI: reuse fallback auth for runtime and discovery
This commit is contained in:
parent
800042a3d5
commit
9dd08a49a4
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue