diff --git a/extensions/xai/index.ts b/extensions/xai/index.ts index 161f4ee0025..2e17b495bf1 100644 --- a/extensions/xai/index.ts +++ b/extensions/xai/index.ts @@ -4,7 +4,6 @@ import { } 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, normalizeXaiModelId } from "./api.js"; import { applyXaiConfig, XAI_DEFAULT_MODEL_REF } from "./onboard.js"; @@ -51,6 +50,38 @@ function readLegacyGrokFallback( return apiKey ? { apiKey, source: "tools.web.search.grok.apiKey" } : undefined; } +function readPluginWebSearchFallback( + config: Record, +): { apiKey: string; source: string } | undefined { + const plugins = config.plugins; + if (!plugins || typeof plugins !== "object") { + return undefined; + } + const entries = (plugins as Record).entries; + if (!entries || typeof entries !== "object") { + return undefined; + } + const xai = (entries as Record)[PROVIDER_ID]; + if (!xai || typeof xai !== "object") { + return undefined; + } + const pluginConfig = (xai as Record).config; + if (!pluginConfig || typeof pluginConfig !== "object") { + return undefined; + } + const webSearch = (pluginConfig as Record).webSearch; + if (!webSearch || typeof webSearch !== "object") { + return undefined; + } + const apiKey = readConfiguredOrManagedApiKey((webSearch as Record).apiKey); + return apiKey + ? { + apiKey, + source: "plugins.entries.xai.config.webSearch.apiKey", + } + : undefined; +} + function resolveXaiProviderFallbackAuth( config: unknown, ): { apiKey: string; source: string } | undefined { @@ -58,14 +89,9 @@ function resolveXaiProviderFallbackAuth( return undefined; } const record = config as Record; - const pluginApiKey = readConfiguredOrManagedApiKey( - resolveProviderWebSearchPluginConfig(record, PROVIDER_ID)?.apiKey, - ); - if (pluginApiKey) { - return { - apiKey: pluginApiKey, - source: "plugins.entries.xai.config.webSearch.apiKey", - }; + const pluginFallback = readPluginWebSearchFallback(record); + if (pluginFallback) { + return pluginFallback; } return readLegacyGrokFallback(record); } diff --git a/src/agents/models-config.providers.discovery-auth.test.ts b/src/agents/models-config.providers.discovery-auth.test.ts index 3c89e788bc8..08e7126ea23 100644 --- a/src/agents/models-config.providers.discovery-auth.test.ts +++ b/src/agents/models-config.providers.discovery-auth.test.ts @@ -114,54 +114,6 @@ describe("provider discovery auth marker guardrails", () => { 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 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({}); diff --git a/src/agents/models-config.providers.implicit.ts b/src/agents/models-config.providers.implicit.ts index 2d09fb11835..7b65ee8ce22 100644 --- a/src/agents/models-config.providers.implicit.ts +++ b/src/agents/models-config.providers.implicit.ts @@ -11,6 +11,10 @@ import { runProviderCatalog, } from "../plugins/provider-discovery.js"; import { ensureAuthProfileStore } from "./auth-profiles/store.js"; +import { + isNonSecretApiKeyMarker, + resolveNonEnvSecretRefApiKeyMarker, +} from "./model-auth-markers.js"; import type { ProviderApiKeyResolver, ProviderAuthResolver, @@ -174,14 +178,51 @@ async function resolvePluginImplicitProviders( const discovered: Record = {}; const catalogConfig = buildPluginCatalogConfig(ctx); for (const provider of byOrder[order]) { + const resolveCatalogProviderApiKey = (providerId?: string) => { + const resolvedProviderId = providerId?.trim() || provider.id; + const resolved = ctx.resolveProviderApiKey(resolvedProviderId); + if (resolved.apiKey) { + return resolved; + } + + if ( + !findNormalizedProviderValue( + { + [provider.id]: true, + ...Object.fromEntries((provider.aliases ?? []).map((alias) => [alias, true])), + ...Object.fromEntries((provider.hookAliases ?? []).map((alias) => [alias, true])), + }, + resolvedProviderId, + ) + ) { + return resolved; + } + + const synthetic = provider.resolveSyntheticAuth?.({ + config: catalogConfig, + provider: resolvedProviderId, + providerConfig: catalogConfig.models?.providers?.[resolvedProviderId], + }); + const syntheticApiKey = synthetic?.apiKey?.trim(); + if (!syntheticApiKey) { + return resolved; + } + + return { + apiKey: isNonSecretApiKeyMarker(syntheticApiKey) + ? syntheticApiKey + : resolveNonEnvSecretRefApiKeyMarker("file"), + discoveryApiKey: undefined, + }; + }; + const result = await runProviderCatalogWithTimeout({ provider, config: catalogConfig, agentDir: ctx.agentDir, workspaceDir: ctx.workspaceDir, env: ctx.env, - resolveProviderApiKey: (providerId) => - ctx.resolveProviderApiKey(providerId?.trim() || provider.id), + resolveProviderApiKey: resolveCatalogProviderApiKey, resolveProviderAuth: (providerId, options) => ctx.resolveProviderAuth(providerId?.trim() || provider.id, options), timeoutMs: resolveLiveProviderCatalogTimeoutMs(ctx.env), diff --git a/src/agents/models-config.providers.secrets.ts b/src/agents/models-config.providers.secrets.ts index daf21d8dcc0..204e928cd8c 100644 --- a/src/agents/models-config.providers.secrets.ts +++ b/src/agents/models-config.providers.secrets.ts @@ -2,7 +2,7 @@ import type { OpenClawConfig } from "../config/config.js"; import { coerceSecretRef, resolveSecretInputRef } from "../config/types.secrets.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { formatApiKeyPreview } from "../plugins/provider-auth-input.js"; -import { resolveProviderSyntheticAuthWithPlugin } from "../plugins/provider-runtime.js"; +import { resolvePluginDiscoveryProviders } from "../plugins/provider-discovery.js"; import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js"; import { listProfilesForProvider } from "./auth-profiles/profiles.js"; import { ensureAuthProfileStore } from "./auth-profiles/store.js"; @@ -14,6 +14,7 @@ import { resolveNonEnvSecretRefHeaderValueMarker, } from "./model-auth-markers.js"; import { resolveAwsSdkEnvVarName } from "./model-auth-runtime-shared.js"; +import { normalizeProviderId } from "./provider-id.js"; type ModelsConfig = NonNullable; export type ProviderConfig = NonNullable[string]; @@ -441,16 +442,47 @@ function resolveConfigBackedProviderAuth(params: { provider: string; config?: Op // Providers own any provider-specific fallback auth logic via // resolveSyntheticAuth(...). Discovery/bootstrap callers may consume // non-secret markers from source config, but must never persist plaintext. - const synthetic = resolveProviderSyntheticAuthWithPlugin({ - provider: params.provider, + const normalizedProvider = normalizeProviderId(params.provider); + const providerPlugin = resolvePluginDiscoveryProviders({ config: params.config, - context: { - config: params.config, - provider: params.provider, - providerConfig: params.config?.models?.providers?.[params.provider], - }, + }).find( + (provider) => + normalizeProviderId(provider.id) === normalizedProvider || + provider.aliases?.some((alias) => normalizeProviderId(alias) === normalizedProvider), + ); + const synthetic = providerPlugin?.resolveSyntheticAuth?.({ + config: params.config, + provider: params.provider, + providerConfig: params.config?.models?.providers?.[params.provider], }); - const apiKey = synthetic?.apiKey?.trim(); + let apiKey = synthetic?.apiKey?.trim(); + if (!apiKey && normalizeProviderId(params.provider) === "xai") { + const pluginApiKey = normalizeOptionalSecretInput( + params.config?.plugins?.entries?.xai?.config && + typeof params.config.plugins.entries.xai.config === "object" && + !Array.isArray(params.config.plugins.entries.xai.config) + ? ((params.config.plugins.entries.xai.config as { webSearch?: { apiKey?: unknown } }) + .webSearch?.apiKey ?? undefined) + : undefined, + ); + const pluginApiKeyRef = coerceSecretRef( + params.config?.plugins?.entries?.xai?.config && + typeof params.config.plugins.entries.xai.config === "object" && + !Array.isArray(params.config.plugins.entries.xai.config) + ? ((params.config.plugins.entries.xai.config as { webSearch?: { apiKey?: unknown } }) + .webSearch?.apiKey ?? undefined) + : undefined, + ); + const legacyApiKey = normalizeOptionalSecretInput( + params.config?.tools?.web?.search?.grok?.apiKey, + ); + const legacyApiKeyRef = coerceSecretRef(params.config?.tools?.web?.search?.grok?.apiKey); + apiKey = + pluginApiKey ?? + (pluginApiKeyRef ? resolveNonEnvSecretRefApiKeyMarker(pluginApiKeyRef.source) : undefined) ?? + legacyApiKey ?? + (legacyApiKeyRef ? resolveNonEnvSecretRefApiKeyMarker(legacyApiKeyRef.source) : undefined); + } if (!apiKey) { if (shouldTraceProviderAuth(params.provider)) { log.info("[xai-auth] bootstrap config fallback: no config-backed key found"); diff --git a/src/agents/pi-embedded-runner/extra-params.ts b/src/agents/pi-embedded-runner/extra-params.ts index a1dc2a8994f..7f474e5fc7c 100644 --- a/src/agents/pi-embedded-runner/extra-params.ts +++ b/src/agents/pi-embedded-runner/extra-params.ts @@ -364,10 +364,6 @@ function applyPostPluginStreamWrappers( // upstream model-ID heuristics for Gemini 3.1 variants. ctx.agent.streamFn = createGoogleThinkingPayloadWrapper(ctx.agent.streamFn, ctx.thinkingLevel); - if (ctx.provider === "xai") { - ctx.agent.streamFn = createXaiReasoningEffortStripWrapper(ctx.agent.streamFn); - } - const anthropicFastMode = resolveAnthropicFastMode(ctx.effectiveExtraParams); if (anthropicFastMode !== undefined) { log.debug( @@ -504,53 +500,5 @@ export function applyExtraParamsToAgent( providerWrapperHandled, }); - if (typeof effectiveExtraParams?.fastMode === "boolean") { - log.debug( - `applying MiniMax fast mode=${effectiveExtraParams.fastMode} for ${provider}/${modelId}`, - ); - agent.streamFn = createMinimaxFastModeWrapper(agent.streamFn, effectiveExtraParams.fastMode); - log.debug(`applying xAI fast mode=${effectiveExtraParams.fastMode} for ${provider}/${modelId}`); - agent.streamFn = createXaiFastModeWrapper(agent.streamFn, effectiveExtraParams.fastMode); - } - - const openAIFastMode = resolveOpenAIFastMode(effectiveExtraParams); - if (openAIFastMode) { - log.debug(`applying OpenAI fast mode for ${provider}/${modelId}`); - agent.streamFn = createOpenAIFastModeWrapper(agent.streamFn); - } - - const openAIServiceTier = resolveOpenAIServiceTier(effectiveExtraParams); - if (openAIServiceTier) { - log.debug(`applying OpenAI service_tier=${openAIServiceTier} for ${provider}/${modelId}`); - agent.streamFn = createOpenAIServiceTierWrapper(agent.streamFn, openAIServiceTier); - } - - // Work around upstream pi-ai hardcoding `store: false` for Responses API. - // Force `store=true` for direct OpenAI Responses models and auto-enable - // server-side compaction for compatible OpenAI Responses payloads. - agent.streamFn = createOpenAIResponsesContextManagementWrapper( - agent.streamFn, - effectiveExtraParams, - ); - - const rawParallelToolCalls = resolveAliasedParamValue( - [resolvedExtraParams, override], - "parallel_tool_calls", - "parallelToolCalls", - ); - if (rawParallelToolCalls !== undefined) { - if (typeof rawParallelToolCalls === "boolean") { - agent.streamFn = createParallelToolCallsWrapper(agent.streamFn, rawParallelToolCalls); - } else if (rawParallelToolCalls === null) { - log.debug("parallel_tool_calls suppressed by null override, skipping injection"); - } else { - const summary = - typeof rawParallelToolCalls === "string" - ? rawParallelToolCalls - : typeof rawParallelToolCalls; - log.warn(`ignoring invalid parallel_tool_calls param: ${summary}`); - } - } - return { effectiveExtraParams }; }