xAI: honor config-backed auth during provider bootstrap

This commit is contained in:
huntharo 2026-03-27 13:45:24 -04:00 committed by Peter Steinberger
parent 2d919cf63d
commit d5fafbe3ce
4 changed files with 138 additions and 4 deletions

View File

@ -137,6 +137,32 @@ describe("xai web search config resolution", () => {
});
});
it("reuses the legacy grok web search api key for provider auth fallback", () => {
const captured = capturePluginRegistration(xaiPlugin);
const provider = captured.providers[0];
expect(
provider?.resolveSyntheticAuth?.({
config: {
tools: {
web: {
search: {
grok: {
apiKey: "xai-legacy-fallback", // pragma: allowlist secret
},
},
},
},
},
provider: "xai",
providerConfig: undefined,
}),
).toEqual({
apiKey: "xai-legacy-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");

View File

@ -113,4 +113,50 @@ describe("provider discovery auth marker guardrails", () => {
const request = vllmCall?.[1] as { headers?: Record<string, string> } | undefined;
expect(request?.headers?.Authorization).toBe("Bearer ALLCAPS_SAMPLE");
});
it("surfaces xai provider auth from plugin web search config without persisting plaintext", async () => {
const agentDir = await createAgentDirWithAuthProfiles({});
const providers = await resolveImplicitProvidersForTest({
agentDir,
env: {},
config: {
plugins: {
entries: {
xai: {
config: {
webSearch: {
apiKey: "xai-plugin-config-key", // pragma: allowlist secret
},
},
},
},
},
},
});
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({});
const providers = await resolveImplicitProvidersForTest({
agentDir,
env: {},
config: {
tools: {
web: {
search: {
grok: {
apiKey: "xai-legacy-config-key", // pragma: allowlist secret
},
},
},
},
},
});
expect(providers?.xai?.apiKey).toBe(NON_ENV_SECRETREF_MARKER);
});
});

View File

@ -305,8 +305,8 @@ export async function resolveImplicitProviders(
...params,
authStore,
env,
resolveProviderApiKey: createProviderApiKeyResolver(env, authStore),
resolveProviderAuth: createProviderAuthResolver(env, authStore),
resolveProviderApiKey: createProviderApiKeyResolver(env, authStore, params.config),
resolveProviderAuth: createProviderAuthResolver(env, authStore, params.config),
};
for (const order of PLUGIN_DISCOVERY_ORDERS) {

View File

@ -1,5 +1,6 @@
import type { OpenClawConfig } from "../config/config.js";
import { coerceSecretRef, resolveSecretInputRef } from "../config/types.secrets.js";
import { resolveProviderSyntheticAuthWithPlugin } from "../plugins/provider-runtime.js";
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
import { listProfilesForProvider } from "./auth-profiles/profiles.js";
import { ensureAuthProfileStore } from "./auth-profiles/store.js";
@ -304,6 +305,7 @@ export function resolveMissingProviderApiKey(params: {
export function createProviderApiKeyResolver(
env: NodeJS.ProcessEnv,
authStore: ReturnType<typeof ensureAuthProfileStore>,
config?: OpenClawConfig,
): ProviderApiKeyResolver {
return (provider: string): { apiKey: string | undefined; discoveryApiKey?: string } => {
const envVar = resolveEnvApiKeyVarName(provider, env);
@ -314,9 +316,19 @@ export function createProviderApiKeyResolver(
};
}
const fromProfiles = resolveApiKeyFromProfiles({ provider, store: authStore, env });
if (fromProfiles?.apiKey) {
return {
apiKey: fromProfiles.apiKey,
discoveryApiKey: fromProfiles.discoveryApiKey,
};
}
const fromConfig = resolveConfigBackedProviderAuth({
provider,
config,
});
return {
apiKey: fromProfiles?.apiKey,
discoveryApiKey: fromProfiles?.discoveryApiKey,
apiKey: fromConfig?.apiKey,
discoveryApiKey: fromConfig?.discoveryApiKey,
};
};
}
@ -324,6 +336,7 @@ export function createProviderApiKeyResolver(
export function createProviderAuthResolver(
env: NodeJS.ProcessEnv,
authStore: ReturnType<typeof ensureAuthProfileStore>,
config?: OpenClawConfig,
): ProviderAuthResolver {
return (provider: string, options?: { oauthMarker?: string }) => {
const ids = listProfilesForProvider(authStore, provider);
@ -377,6 +390,19 @@ export function createProviderAuthResolver(
};
}
const fromConfig = resolveConfigBackedProviderAuth({
provider,
config,
});
if (fromConfig) {
return {
apiKey: fromConfig.apiKey,
discoveryApiKey: fromConfig.discoveryApiKey,
mode: fromConfig.mode,
source: "none",
};
}
return {
apiKey: undefined,
discoveryApiKey: undefined,
@ -385,3 +411,39 @@ export function createProviderAuthResolver(
};
};
}
function resolveConfigBackedProviderAuth(params: { provider: string; config?: OpenClawConfig }):
| {
apiKey: string;
discoveryApiKey?: string;
mode: "api_key";
source: "config";
}
| undefined {
const synthetic = resolveProviderSyntheticAuthWithPlugin({
provider: params.provider,
config: params.config,
context: {
config: params.config,
provider: params.provider,
providerConfig: params.config?.models?.providers?.[params.provider],
},
});
const apiKey = synthetic?.apiKey?.trim();
if (!apiKey) {
return undefined;
}
return isNonSecretApiKeyMarker(apiKey)
? {
apiKey,
discoveryApiKey: toDiscoveryApiKey(apiKey),
mode: "api_key",
source: "config",
}
: {
apiKey: resolveNonEnvSecretRefApiKeyMarker("file"),
discoveryApiKey: toDiscoveryApiKey(apiKey),
mode: "api_key",
source: "config",
};
}