fix(providers): stabilize runtime normalization hooks

This commit is contained in:
Peter Steinberger
2026-04-04 19:33:30 +01:00
parent e06e36d41a
commit ca200eb480
19 changed files with 196 additions and 126 deletions

View File

@@ -1,5 +1,5 @@
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { registerAmazonBedrockPlugin } from "./register.runtime.js";
import { registerAmazonBedrockPlugin } from "./register.sync.runtime.js";
export default definePluginEntry({
id: "amazon-bedrock",

View File

@@ -1,10 +1,16 @@
import type { StreamFn } from "@mariozechner/pi-agent-core";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import {
createBedrockNoCacheWrapper,
isAnthropicBedrockModel,
streamWithPayloadPatch,
} from "openclaw/plugin-sdk/provider-stream";
import {
mergeImplicitBedrockProvider,
resolveBedrockConfigApiKey,
resolveImplicitBedrockProvider,
} from "./api.js";
type GuardrailConfig = {
guardrailIdentifier: string;
@@ -38,7 +44,7 @@ function createGuardrailWrapStreamFn(
};
}
export async function registerAmazonBedrockPlugin(api: OpenClawPluginApi): Promise<void> {
export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
// Keep registration-local constants inside the function so partial module
// initialization during test bootstrap cannot trip TDZ reads.
const providerId = "amazon-bedrock";
@@ -48,15 +54,6 @@ export async function registerAmazonBedrockPlugin(api: OpenClawPluginApi): Promi
/ValidationException.*(?:exceeds? the (?:maximum|max) (?:number of )?(?:input )?tokens)/i,
/ModelStreamErrorException.*(?:Input is too long|too many input tokens)/i,
] as const;
// Defer provider-owned helper loading until registration so test/plugin-loader
// cycles cannot re-enter this module before its constants initialize.
const [
{ buildProviderReplayFamilyHooks },
{ mergeImplicitBedrockProvider, resolveBedrockConfigApiKey, resolveImplicitBedrockProvider },
] = await Promise.all([
import("openclaw/plugin-sdk/provider-model-shared"),
import("./api.js"),
]);
const anthropicByModelReplayHooks = buildProviderReplayFamilyHooks({
family: "anthropic-by-model",
});

View File

@@ -6,6 +6,6 @@ export default definePluginEntry({
name: "Anthropic Provider",
description: "Bundled Anthropic provider plugin",
register(api) {
registerAnthropicPlugin(api);
return registerAnthropicPlugin(api);
},
});

View File

@@ -7,6 +7,7 @@ import type {
} from "openclaw/plugin-sdk/plugin-entry";
import {
applyAuthProfileConfig,
createProviderApiKeyAuthMethod,
ensureApiKeyFromOptionEnvOrPrompt,
listProfilesForProvider,
normalizeApiKeyInput,
@@ -17,11 +18,13 @@ import {
} from "openclaw/plugin-sdk/provider-auth";
import { cloneFirstTemplateModel } from "openclaw/plugin-sdk/provider-model-shared";
import { fetchClaudeUsage } from "openclaw/plugin-sdk/provider-usage";
import { buildAnthropicCliBackend } from "./cli-backend.js";
import { buildAnthropicCliMigrationResult, hasClaudeCliAuth } from "./cli-migration.js";
import {
applyAnthropicConfigDefaults,
normalizeAnthropicProviderConfig,
} from "./config-defaults.js";
import { anthropicMediaUnderstandingProvider } from "./media-understanding-provider.js";
import { buildAnthropicReplayPolicy } from "./replay-policy.js";
import { wrapAnthropicProviderStream } from "./stream-wrappers.js";
@@ -200,7 +203,7 @@ async function runAnthropicCliMigrationNonInteractive(ctx: {
};
}
export async function registerAnthropicPlugin(api: OpenClawPluginApi): Promise<void> {
export function registerAnthropicPlugin(api: OpenClawPluginApi): void {
const claudeCliProfileId = "anthropic:claude-cli";
const providerId = "anthropic";
const defaultAnthropicModel = "anthropic/claude-sonnet-4-6";
@@ -211,45 +214,7 @@ export async function registerAnthropicPlugin(api: OpenClawPluginApi): Promise<v
"anthropic/claude-sonnet-4-5",
"anthropic/claude-haiku-4-5",
] as const;
let createApiKeyAuthMethod:
| (typeof import("openclaw/plugin-sdk/provider-auth-api-key"))["createProviderApiKeyAuthMethod"]
| undefined;
let mediaUnderstandingProvider:
| (typeof import("./media-understanding-provider.js"))["anthropicMediaUnderstandingProvider"]
| undefined;
// Avoid touching a partially initialized static binding during cyclic bootstrap.
try {
const cliBackendModule = await import("./cli-backend.js");
const cliBackend =
typeof cliBackendModule.buildAnthropicCliBackend === "function"
? cliBackendModule.buildAnthropicCliBackend()
: undefined;
if (cliBackend) {
api.registerCliBackend(cliBackend);
}
} catch {
// Best-effort during test bootstrap; provider registration still proceeds.
}
try {
const providerApiKeyAuthModule = await import("openclaw/plugin-sdk/provider-auth-api-key");
createApiKeyAuthMethod =
typeof providerApiKeyAuthModule.createProviderApiKeyAuthMethod === "function"
? providerApiKeyAuthModule.createProviderApiKeyAuthMethod
: undefined;
} catch {
createApiKeyAuthMethod = undefined;
}
if (!createApiKeyAuthMethod) {
return;
}
try {
const mediaUnderstandingModule = await import("./media-understanding-provider.js");
mediaUnderstandingProvider =
mediaUnderstandingModule.anthropicMediaUnderstandingProvider ?? undefined;
} catch {
mediaUnderstandingProvider = undefined;
}
api.registerCliBackend(buildAnthropicCliBackend());
api.registerProvider({
id: providerId,
label: "Anthropic",
@@ -291,7 +256,7 @@ export async function registerAnthropicPlugin(api: OpenClawPluginApi): Promise<v
runtime: ctx.runtime,
}),
},
createApiKeyAuthMethod({
createProviderApiKeyAuthMethod({
providerId,
methodId: "api-key",
label: "Anthropic API key",
@@ -337,7 +302,5 @@ export async function registerAnthropicPlugin(api: OpenClawPluginApi): Promise<v
profileId: ctx.profileId,
}),
});
if (mediaUnderstandingProvider) {
api.registerMediaUnderstandingProvider(mediaUnderstandingProvider);
}
api.registerMediaUnderstandingProvider(anthropicMediaUnderstandingProvider);
}

View File

@@ -1,4 +1,16 @@
import { definePluginEntry, type ProviderAuthContext } from "openclaw/plugin-sdk/plugin-entry";
import {
coerceSecretRef,
DEFAULT_COPILOT_API_BASE_URL,
ensureAuthProfileStore,
fetchCopilotUsage,
githubCopilotLoginCommand,
listProfilesForProvider,
PROVIDER_ID,
resolveCopilotApiToken,
resolveCopilotForwardCompatModel,
wrapCopilotProviderStream,
} from "./register.runtime.js";
const COPILOT_ENV_VARS = ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"];
const COPILOT_XHIGH_MODEL_IDS = ["gpt-5.2", "gpt-5.2-codex"] as const;
@@ -15,19 +27,7 @@ export default definePluginEntry({
id: "github-copilot",
name: "GitHub Copilot Provider",
description: "Bundled GitHub Copilot provider plugin",
async register(api) {
const {
coerceSecretRef,
DEFAULT_COPILOT_API_BASE_URL,
ensureAuthProfileStore,
fetchCopilotUsage,
githubCopilotLoginCommand,
listProfilesForProvider,
PROVIDER_ID,
resolveCopilotApiToken,
resolveCopilotForwardCompatModel,
wrapCopilotProviderStream,
} = await import("./register.runtime.js");
register(api) {
function resolveFirstGithubToken(params: { agentDir?: string; env: NodeJS.ProcessEnv }): {
githubToken: string;
hasProfile: boolean;

View File

@@ -1,10 +1,24 @@
export {
import {
coerceSecretRef,
ensureAuthProfileStore,
listProfilesForProvider,
} from "openclaw/plugin-sdk/provider-auth";
export { githubCopilotLoginCommand } from "openclaw/plugin-sdk/provider-auth-login";
export { PROVIDER_ID, resolveCopilotForwardCompatModel } from "./models.js";
export { wrapCopilotAnthropicStream, wrapCopilotProviderStream } from "./stream.js";
export { DEFAULT_COPILOT_API_BASE_URL, resolveCopilotApiToken } from "./token.js";
export { fetchCopilotUsage } from "./usage.js";
import { githubCopilotLoginCommand } from "openclaw/plugin-sdk/provider-auth-login";
import { PROVIDER_ID, resolveCopilotForwardCompatModel } from "./models.js";
import { wrapCopilotAnthropicStream, wrapCopilotProviderStream } from "./stream.js";
import { DEFAULT_COPILOT_API_BASE_URL, resolveCopilotApiToken } from "./token.js";
import { fetchCopilotUsage } from "./usage.js";
export {
coerceSecretRef,
DEFAULT_COPILOT_API_BASE_URL,
ensureAuthProfileStore,
fetchCopilotUsage,
githubCopilotLoginCommand,
listProfilesForProvider,
PROVIDER_ID,
resolveCopilotApiToken,
resolveCopilotForwardCompatModel,
wrapCopilotAnthropicStream,
wrapCopilotProviderStream,
};

View File

@@ -5,6 +5,18 @@ import {
type ProviderRuntimeModel,
type ProviderWrapStreamFnContext,
} from "openclaw/plugin-sdk/plugin-entry";
import {
applyOpenrouterConfig,
buildOpenrouterProvider,
buildProviderReplayFamilyHooks,
buildProviderStreamFamilyHooks,
createProviderApiKeyAuthMethod,
DEFAULT_CONTEXT_TOKENS,
getOpenRouterModelCapabilities,
loadOpenRouterModelCapabilities,
OPENROUTER_DEFAULT_MODEL_REF,
openrouterMediaUnderstandingProvider,
} from "./register.runtime.js";
const PROVIDER_ID = "openrouter";
const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
@@ -20,19 +32,7 @@ export default definePluginEntry({
id: "openrouter",
name: "OpenRouter Provider",
description: "Bundled OpenRouter provider plugin",
async register(api) {
const {
buildProviderReplayFamilyHooks,
buildProviderStreamFamilyHooks,
createProviderApiKeyAuthMethod,
DEFAULT_CONTEXT_TOKENS,
getOpenRouterModelCapabilities,
loadOpenRouterModelCapabilities,
OPENROUTER_DEFAULT_MODEL_REF,
openrouterMediaUnderstandingProvider,
applyOpenrouterConfig,
buildOpenrouterProvider,
} = await import("./register.runtime.js");
register(api) {
const PASSTHROUGH_GEMINI_REPLAY_HOOKS = buildProviderReplayFamilyHooks({
family: "passthrough-gemini",
});

View File

@@ -1,9 +1,9 @@
export { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
export {
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
import {
buildProviderReplayFamilyHooks,
DEFAULT_CONTEXT_TOKENS,
} from "openclaw/plugin-sdk/provider-model-shared";
export {
import {
buildProviderStreamFamilyHooks,
createOpenRouterSystemCacheWrapper,
createOpenRouterWrapper,
@@ -11,6 +11,22 @@ export {
isProxyReasoningUnsupported,
loadOpenRouterModelCapabilities,
} from "openclaw/plugin-sdk/provider-stream";
export { openrouterMediaUnderstandingProvider } from "./media-understanding-provider.js";
export { applyOpenrouterConfig, OPENROUTER_DEFAULT_MODEL_REF } from "./onboard.js";
export { buildOpenrouterProvider } from "./provider-catalog.js";
import { openrouterMediaUnderstandingProvider } from "./media-understanding-provider.js";
import { applyOpenrouterConfig, OPENROUTER_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildOpenrouterProvider } from "./provider-catalog.js";
export {
applyOpenrouterConfig,
buildOpenrouterProvider,
buildProviderReplayFamilyHooks,
buildProviderStreamFamilyHooks,
createOpenRouterSystemCacheWrapper,
createOpenRouterWrapper,
createProviderApiKeyAuthMethod,
DEFAULT_CONTEXT_TOKENS,
getOpenRouterModelCapabilities,
isProxyReasoningUnsupported,
loadOpenRouterModelCapabilities,
OPENROUTER_DEFAULT_MODEL_REF,
openrouterMediaUnderstandingProvider,
};

View File

@@ -2,3 +2,8 @@ export {
buildChannelConfigSchema,
TelegramConfigSchema,
} from "openclaw/plugin-sdk/channel-config-schema";
export {
normalizeTelegramCommandDescription,
normalizeTelegramCommandName,
resolveTelegramCustomCommands,
} from "./src/command-config.js";

View File

@@ -357,6 +357,7 @@ export function getSoonestCooldownExpiry(
): number | null {
const ts = options?.now ?? Date.now();
let soonest: number | null = null;
let latestMatchingModelCooldown: number | null = null;
for (const id of profileIds) {
const stats = store.usageStats?.[id];
if (!stats) {
@@ -369,11 +370,27 @@ export function getSoonestCooldownExpiry(
if (typeof until !== "number" || !Number.isFinite(until) || until <= 0) {
continue;
}
const matchingModelScopedCooldown =
options?.forModel &&
stats.cooldownReason === "rate_limit" &&
stats.cooldownModel === options.forModel &&
!isActiveUnusableWindow(stats.disabledUntil, ts);
if (matchingModelScopedCooldown) {
latestMatchingModelCooldown =
latestMatchingModelCooldown === null ? until : Math.max(latestMatchingModelCooldown, until);
continue;
}
if (soonest === null || until < soonest) {
soonest = until;
}
}
return soonest;
if (soonest === null) {
return latestMatchingModelCooldown;
}
if (latestMatchingModelCooldown === null) {
return soonest;
}
return Math.min(soonest, latestMatchingModelCooldown);
}
function shouldBypassModelScopedCooldown(

View File

@@ -8,12 +8,20 @@ export type ProviderModelRef = {
export function resolveConfiguredProviderFallback(params: {
cfg: Pick<OpenClawConfig, "models">;
defaultProvider: string;
defaultModel?: string;
}): ProviderModelRef | null {
const configuredProviders = params.cfg.models?.providers;
if (!configuredProviders || typeof configuredProviders !== "object") {
return null;
}
if (configuredProviders[params.defaultProvider]) {
const defaultProviderConfig = configuredProviders[params.defaultProvider];
const defaultModel = params.defaultModel?.trim();
const defaultProviderHasDefaultModel =
!!defaultProviderConfig &&
!!defaultModel &&
Array.isArray(defaultProviderConfig.models) &&
defaultProviderConfig.models.some((model) => model?.id === defaultModel);
if (defaultProviderConfig && (!defaultModel || defaultProviderHasDefaultModel)) {
return null;
}
const availableProvider = Object.entries(configuredProviders).find(

View File

@@ -404,6 +404,7 @@ export function resolveConfiguredModelRef(params: {
const fallbackProvider = resolveConfiguredProviderFallback({
cfg: params.cfg,
defaultProvider: params.defaultProvider,
defaultModel: params.defaultModel,
});
if (fallbackProvider) {
return fallbackProvider;

View File

@@ -1,7 +1,11 @@
import { afterEach, beforeEach, vi } from "vitest";
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js";
import { resetPluginLoaderTestStateForTest } from "../plugins/loader.test-fixtures.js";
import { resetProviderRuntimeHookCacheForTest } from "../plugins/provider-runtime.js";
import type { MockFn } from "../test-utils/vitest-mock-fn.js";
import { resetModelsJsonReadyCacheForTest } from "./models-config.js";
import { resolveImplicitProviders } from "./models-config.providers.implicit.js";
export function withModelsTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
@@ -14,10 +18,16 @@ export function installModelsConfigTestHooks(opts?: { restoreFetch?: boolean })
beforeEach(() => {
previousHome = process.env.HOME;
resetPluginLoaderTestStateForTest();
resetModelsJsonReadyCacheForTest();
resetProviderRuntimeHookCacheForTest();
});
afterEach(() => {
process.env.HOME = previousHome;
resetPluginLoaderTestStateForTest();
resetModelsJsonReadyCacheForTest();
resetProviderRuntimeHookCacheForTest();
if (opts?.restoreFetch && originalFetch) {
globalThis.fetch = originalFetch;
}
@@ -103,6 +113,7 @@ export const MODELS_CONFIG_IMPLICIT_ENV_VARS = [
"OPENROUTER_API_KEY",
"PI_CODING_AGENT_DIR",
"QIANFAN_API_KEY",
"QWEN_API_KEY",
"MODELSTUDIO_API_KEY",
"SYNTHETIC_API_KEY",
"STEPFUN_API_KEY",
@@ -113,6 +124,7 @@ export const MODELS_CONFIG_IMPLICIT_ENV_VARS = [
"KIMI_API_KEY",
"KIMICODE_API_KEY",
"GEMINI_API_KEY",
"OPENCLAW_BUNDLED_PLUGINS_DIR",
"GOOGLE_APPLICATION_CREDENTIALS",
"GOOGLE_CLOUD_LOCATION",
"GOOGLE_CLOUD_PROJECT",
@@ -146,6 +158,12 @@ export function snapshotImplicitProviderEnv(env?: NodeJS.ProcessEnv): NodeJS.Pro
}
}
// Provider discovery tests can temporarily scrub VITEST/NODE_ENV to exercise
// live HTTP paths. Keep the bundled plugin root pinned to the source checkout
// so those tests do not fall back to potentially stale dist-runtime wrappers.
snapshot.OPENCLAW_BUNDLED_PLUGINS_DIR ??=
resolveBundledPluginsDir({ VITEST: "true" } as NodeJS.ProcessEnv) ?? undefined;
return snapshot;
}

View File

@@ -1,9 +1,10 @@
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import type { ModelDefinitionConfig } from "../config/types.models.js";
import { installModelsConfigTestHooks, withModelsTempHome } from "./models-config.e2e-harness.js";
import { ensureOpenClawModelsJson } from "./models-config.js";
import { readGeneratedModelsJson } from "./models-config.test-utils.js";
function createGoogleModelsConfig(models: ModelDefinitionConfig[]): OpenClawConfig {
return {
@@ -20,18 +21,19 @@ function createGoogleModelsConfig(models: ModelDefinitionConfig[]): OpenClawConf
};
}
async function readGeneratedProvider(providerKey: string) {
const parsed = await readGeneratedModelsJson<{
async function readGeneratedProvider(agentDir: string, providerKey: string) {
const parsed = JSON.parse(await fs.readFile(path.join(agentDir, "models.json"), "utf8")) as {
providers: Record<string, { baseUrl?: string; models: Array<{ id: string }> }>;
}>();
};
return parsed.providers[providerKey];
}
async function expectGeneratedProvider(
agentDir: string,
providerKey: string,
params: { ids: string[]; baseUrl?: string },
) {
const provider = await readGeneratedProvider(providerKey);
const provider = await readGeneratedProvider(agentDir, providerKey);
expect(provider?.models?.map((model) => model.id)).toEqual(params.ids);
if (params.baseUrl !== undefined) {
expect(provider?.baseUrl).toBe(params.baseUrl);
@@ -66,8 +68,8 @@ describe("models-config", () => {
},
]);
await ensureOpenClawModelsJson(cfg);
await expectGeneratedProvider("google", {
const { agentDir } = await ensureOpenClawModelsJson(cfg);
await expectGeneratedProvider(agentDir, "google", {
ids: ["gemini-3-pro-preview", "gemini-3-flash-preview"],
});
});
@@ -88,8 +90,8 @@ describe("models-config", () => {
},
]);
await ensureOpenClawModelsJson(cfg);
await expectGeneratedProvider("google", {
const { agentDir } = await ensureOpenClawModelsJson(cfg);
await expectGeneratedProvider(agentDir, "google", {
ids: ["gemini-3-flash-preview"],
});
});
@@ -121,8 +123,8 @@ describe("models-config", () => {
},
} satisfies OpenClawConfig;
await ensureOpenClawModelsJson(cfg);
await expectGeneratedProvider("google-paid", {
const { agentDir } = await ensureOpenClawModelsJson(cfg);
await expectGeneratedProvider(agentDir, "google-paid", {
ids: ["gemini-3-pro-preview"],
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
});
@@ -154,8 +156,8 @@ describe("models-config", () => {
},
} satisfies OpenClawConfig;
await ensureOpenClawModelsJson(cfg);
await expectGeneratedProvider("google", {
const { agentDir } = await ensureOpenClawModelsJson(cfg);
await expectGeneratedProvider(agentDir, "google", {
ids: ["gemini-3-flash-preview"],
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
});

View File

@@ -1,12 +1,8 @@
import { beforeAll, describe, expect, it } from "vitest";
let normalizeProviderSpecificConfig: typeof import("./models-config.providers.policy.js").normalizeProviderSpecificConfig;
let resolveProviderConfigApiKeyResolver: typeof import("./models-config.providers.policy.js").resolveProviderConfigApiKeyResolver;
beforeAll(async () => {
({ normalizeProviderSpecificConfig, resolveProviderConfigApiKeyResolver } =
await import("./models-config.providers.policy.js"));
});
import { describe, expect, it } from "vitest";
import {
normalizeProviderSpecificConfig,
resolveProviderConfigApiKeyResolver,
} from "./models-config.providers.policy.js";
describe("models-config.providers.policy", () => {
it("resolves config apiKey markers through provider plugin hooks", async () => {

View File

@@ -1,3 +1,8 @@
import { resolveBedrockConfigApiKey } from "../plugin-sdk/amazon-bedrock.js";
import {
normalizeGoogleProviderConfig,
shouldNormalizeGoogleProviderConfig,
} from "../plugin-sdk/google.js";
import {
applyProviderNativeStreamingUsageCompatWithPlugin,
normalizeProviderConfigWithPlugin,
@@ -32,20 +37,32 @@ export function normalizeProviderSpecificConfig(
providerKey: string,
provider: ProviderConfig,
): ProviderConfig {
return (
const normalized =
normalizeProviderConfigWithPlugin({
provider: providerKey,
context: {
provider: providerKey,
providerConfig: provider,
},
}) ?? provider
);
}) ?? undefined;
if (normalized) {
return normalized;
}
if (shouldNormalizeGoogleProviderConfig(providerKey, provider)) {
return normalizeGoogleProviderConfig(providerKey, provider);
}
return provider;
}
export function resolveProviderConfigApiKeyResolver(
providerKey: string,
): ((env: NodeJS.ProcessEnv) => string | undefined) | undefined {
if (providerKey.trim() === "amazon-bedrock") {
return (env) => {
const resolved = resolveBedrockConfigApiKey(env);
return resolved?.trim() || undefined;
};
}
if (!resolveProviderRuntimePlugin({ provider: providerKey })?.resolveConfigApiKey) {
return undefined;
}

View File

@@ -789,9 +789,7 @@ describe("applyExtraParamsToAgent", () => {
void agent.streamFn?.(model, context, {});
expect(payloads).toHaveLength(1);
expect(payloads[0]).toEqual({
reasoning: { effort: "none", summary: "auto" },
});
expect(payloads[0]).not.toHaveProperty("reasoning");
});
it("injects parallel_tool_calls for openai-completions payloads when configured", () => {

View File

@@ -3,7 +3,7 @@ import {
normalizeTelegramCommandDescription,
normalizeTelegramCommandName,
resolveTelegramCustomCommands,
} from "../../extensions/telegram/api.js";
} from "../../extensions/telegram/config-api.js";
import { isSafeScpRemoteHost } from "../infra/scp-host.js";
import { isValidInboundPathRootPattern } from "../media/inbound-path-policy.js";
import { ToolPolicySchema } from "./zod-schema.agent-runtime.js";

View File

@@ -412,7 +412,25 @@ export function normalizeProviderConfigWithPlugin(params: {
env?: NodeJS.ProcessEnv;
context: ProviderNormalizeConfigContext;
}): ModelProviderConfig | undefined {
return resolveProviderHookPlugin(params)?.normalizeConfig?.(params.context) ?? undefined;
const hasConfigChange = (normalized: ModelProviderConfig) =>
normalized !== params.context.providerConfig;
const matchedPlugin = resolveProviderHookPlugin(params);
const normalizedMatched = matchedPlugin?.normalizeConfig?.(params.context);
if (normalizedMatched && hasConfigChange(normalizedMatched)) {
return normalizedMatched;
}
for (const candidate of resolveProviderPluginsForHooks(params)) {
if (!candidate.normalizeConfig || candidate === matchedPlugin) {
continue;
}
const normalized = candidate.normalizeConfig(params.context);
if (normalized && hasConfigChange(normalized)) {
return normalized;
}
}
return undefined;
}
export function applyProviderNativeStreamingUsageCompatWithPlugin(params: {