Skip to content

Commit 42398b1

Browse files
committed
fix(plugins): ignore throwing provider runtime hooks
1 parent 01124cf commit 42398b1

2 files changed

Lines changed: 122 additions & 5 deletions

File tree

src/plugins/provider-runtime.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,38 @@ describe("provider-runtime", () => {
13291329
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(1);
13301330
});
13311331

1332+
it("ignores throwing model-id normalization hooks", () => {
1333+
resolvePluginProvidersMock.mockReturnValue([
1334+
{
1335+
id: "fuzz-provider",
1336+
pluginId: "fuzzplugin\nWARN forged",
1337+
label: "Fuzz Provider",
1338+
aliases: ["fuzz"],
1339+
auth: [],
1340+
normalizeModelId: () => {
1341+
throw new Error("bad\nmodel hook");
1342+
},
1343+
},
1344+
]);
1345+
1346+
expect(
1347+
normalizeProviderModelIdWithPlugin({
1348+
provider: "fuzz",
1349+
context: {
1350+
provider: "fuzz",
1351+
modelId: "demo-model",
1352+
},
1353+
}),
1354+
).toBeUndefined();
1355+
1356+
expect(providerRuntimeWarnMock).toHaveBeenCalledTimes(1);
1357+
const warning = firstMockStringArg(providerRuntimeWarnMock, "provider warning");
1358+
expect(warning).toContain('Provider plugin "fuzzpluginWARN forged"');
1359+
expect(warning).toContain("normalizeModelId hook failed");
1360+
expect(warning).toContain("badmodel hook");
1361+
expect(warning).not.toContain("\n");
1362+
});
1363+
13321364
it("resolves config hooks through hook-only aliases without changing provider surfaces", () => {
13331365
resolvePluginProvidersMock.mockReturnValue([
13341366
{
@@ -1662,6 +1694,50 @@ describe("provider-runtime", () => {
16621694
});
16631695
});
16641696

1697+
it("keeps scanning transport hooks after one provider hook throws", () => {
1698+
resolvePluginProvidersMock.mockImplementation((params) => {
1699+
const plugins: ProviderPlugin[] = [
1700+
{
1701+
id: "custom-fuzz",
1702+
pluginId: "fuzzplugin",
1703+
label: "Fuzz Provider",
1704+
auth: [],
1705+
normalizeTransport: () => {
1706+
throw new Error("bad transport hook");
1707+
},
1708+
},
1709+
{
1710+
id: "fuzz-transport-family",
1711+
pluginId: "fuzzplugin",
1712+
label: "Fuzz Transport Family",
1713+
auth: [],
1714+
normalizeTransport: ({ api, baseUrl }) =>
1715+
api === "openai-completions" ? { api: "openai-responses", baseUrl } : undefined,
1716+
},
1717+
];
1718+
return params.providerRefs?.includes("custom-fuzz") ? [plugins[0]] : plugins;
1719+
});
1720+
1721+
expect(
1722+
normalizeProviderTransportWithPlugin({
1723+
provider: "custom-fuzz",
1724+
context: {
1725+
provider: "custom-fuzz",
1726+
api: "openai-completions",
1727+
baseUrl: "https://api.example.com/v1",
1728+
},
1729+
}),
1730+
).toEqual({
1731+
api: "openai-responses",
1732+
baseUrl: "https://api.example.com/v1",
1733+
});
1734+
1735+
expect(providerRuntimeWarnMock).toHaveBeenCalledTimes(1);
1736+
expect(firstMockStringArg(providerRuntimeWarnMock, "provider warning")).toContain(
1737+
'Provider plugin "fuzzplugin" normalizeTransport hook failed',
1738+
);
1739+
});
1740+
16651741
it("invalidates cached runtime providers when config mutates in place", () => {
16661742
const config = {
16671743
plugins: {

src/plugins/provider-runtime.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import type { ProviderSystemPromptContribution } from "../agents/system-prompt-contribution.js";
1515
import type { ModelProviderConfig } from "../config/types.js";
1616
import type { OpenClawConfig } from "../config/types.openclaw.js";
17+
import { formatErrorMessage } from "../infra/errors.js";
1718
import { createSubsystemLogger } from "../logging/subsystem.js";
1819
import { normalizeProviderModelIdWithManifest } from "./manifest-model-id-normalization.js";
1920
import { resolvePluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
@@ -123,6 +124,29 @@ function matchesAnyProviderPluginRef(provider: ProviderPlugin, providerRefs: rea
123124
return providerRefs.some((providerRef) => matchesProviderPluginRef(provider, providerRef));
124125
}
125126

127+
function getProviderRuntimePluginKey(plugin: ProviderPlugin): string {
128+
return `${plugin.pluginId ?? ""}:${plugin.id}`;
129+
}
130+
131+
function runProviderRuntimeHook<TResult>(params: {
132+
plugin: ProviderPlugin | undefined;
133+
hookName: string;
134+
run: (plugin: ProviderPlugin) => TResult;
135+
}): TResult | undefined {
136+
if (!params.plugin) {
137+
return undefined;
138+
}
139+
try {
140+
return params.run(params.plugin);
141+
} catch (error) {
142+
const pluginId = params.plugin.pluginId ?? params.plugin.id;
143+
log.warn(
144+
`Provider plugin "${sanitizeForLog(pluginId)}" ${sanitizeForLog(params.hookName)} hook failed; ignoring hook: ${sanitizeForLog(formatErrorMessage(error))}`,
145+
);
146+
return undefined;
147+
}
148+
}
149+
126150
function hasExplicitProviderRuntimePluginActivation(params: {
127151
provider: string;
128152
config?: OpenClawConfig;
@@ -369,8 +393,13 @@ export function normalizeProviderModelIdWithPlugin(params: {
369393
}): string | undefined {
370394
const plugin = resolveProviderHookPlugin(params);
371395
return (
372-
normalizeOptionalString(plugin?.normalizeModelId?.(params.context)) ??
373-
normalizeProviderModelIdWithManifest(params)
396+
normalizeOptionalString(
397+
runProviderRuntimeHook({
398+
plugin,
399+
hookName: "normalizeModelId",
400+
run: (providerPlugin) => providerPlugin.normalizeModelId?.(params.context),
401+
}),
402+
) ?? normalizeProviderModelIdWithManifest(params)
374403
);
375404
}
376405

@@ -385,16 +414,28 @@ export function normalizeProviderTransportWithPlugin(params: {
385414
(normalized.api ?? params.context.api) !== params.context.api ||
386415
(normalized.baseUrl ?? params.context.baseUrl) !== params.context.baseUrl;
387416
const matchedPlugin = resolveProviderHookPlugin(params);
388-
const normalizedMatched = matchedPlugin?.normalizeTransport?.(params.context);
417+
const normalizedMatched = runProviderRuntimeHook({
418+
plugin: matchedPlugin,
419+
hookName: "normalizeTransport",
420+
run: (providerPlugin) => providerPlugin.normalizeTransport?.(params.context),
421+
});
422+
const matchedPluginKey = matchedPlugin ? getProviderRuntimePluginKey(matchedPlugin) : undefined;
389423
if (normalizedMatched && hasTransportChange(normalizedMatched)) {
390424
return normalizedMatched;
391425
}
392426

393427
for (const candidate of resolveProviderPluginsForHooks(params)) {
394-
if (!candidate.normalizeTransport || candidate === matchedPlugin) {
428+
if (
429+
!candidate.normalizeTransport ||
430+
(matchedPluginKey && getProviderRuntimePluginKey(candidate) === matchedPluginKey)
431+
) {
395432
continue;
396433
}
397-
const normalized = candidate.normalizeTransport(params.context);
434+
const normalized = runProviderRuntimeHook({
435+
plugin: candidate,
436+
hookName: "normalizeTransport",
437+
run: (providerPlugin) => providerPlugin.normalizeTransport?.(params.context),
438+
});
398439
if (normalized && hasTransportChange(normalized)) {
399440
return normalized;
400441
}

0 commit comments

Comments
 (0)