mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 21:00:44 +00:00
refactor(models): move suppressions to manifests
This commit is contained in:
@@ -11,7 +11,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugin SDK: expose shared channel route normalization, parser-driven target resolution, raw-target compact keys, parsed-target types, and route comparison helpers through `openclaw/plugin-sdk/channel-route`, switch native approval origin matching onto that route contract with optional delivery and match-only target normalization, and retire the internal channel-route shim behind dated compatibility aliases for legacy key/comparable-target helpers. Thanks @vincentkoc.
|
||||
- Docs/Codex: document how Codex Computer Use, direct `cua-driver mcp`, and OpenClaw.app's PeekabooBridge fit together so desktop-control setup choices are clearer. Thanks @pash-openai and @trycua.
|
||||
- Matrix/streaming: stream tool-progress updates into live Matrix preview edits by default when preview streaming is active, with `streaming.preview.toolProgress: false` to keep answer previews while hiding interim tool lines. Thanks @gumadeiras.
|
||||
- Plugins/models: wire manifest `modelCatalog.aliases` and `modelCatalog.suppressions` into model-catalog planning and built-in model suppression, with OpenAI stale Spark suppression now declared in the plugin manifest before runtime fallback. Thanks @shakkernerd.
|
||||
- Plugins/models: wire manifest `modelCatalog.aliases` and `modelCatalog.suppressions` into model-catalog planning and built-in model suppression, with stale Spark and Qwen Coding Plan suppressions now declared in plugin manifests instead of runtime fallback hooks. Thanks @shakkernerd.
|
||||
- Channels/Yuanbao: register the Tencent Yuanbao external channel plugin (`openclaw-plugin-yuanbao`) in the official channel catalog, contract suites, and community plugin docs, with a new `docs/channels/yuanbao.md` quick-start guide for WebSocket bot DMs and group chats. (#72756) Thanks @loongfay.
|
||||
- Channels/QQBot: add full group chat support (history tracking, @-mention gating, activation modes, per-group config, FIFO message queue with deliver debounce), C2C `stream_messages` streaming with a `StreamingController` lifecycle manager, unified `sendMedia` with chunked upload for large files, and refactor the engine into pipeline stages, focused outbound submodules, builtin slash-command modules, and explicit DI ports via `createEngineAdapters()`. (#70624) Thanks @cxyhhhhh.
|
||||
- Gateway/startup: reuse lookup-table plugin manifests when loading startup plugins so Gateway boot avoids rebuilding plugin discovery and manifest metadata. Thanks @shakkernerd.
|
||||
|
||||
@@ -248,7 +248,7 @@ The "When to use" column is the quick decision guide.
|
||||
| 28 | `classifyFailoverReason` | Provider-owned failover reason classification | Provider can map raw API/transport errors to rate-limit/overload/etc |
|
||||
| 29 | `isCacheTtlEligible` | Prompt-cache policy for proxy/backhaul providers | Provider needs proxy-specific cache TTL gating |
|
||||
| 30 | `buildMissingAuthMessage` | Replacement for the generic missing-auth recovery message | Provider needs a provider-specific missing-auth recovery hint |
|
||||
| 31 | `suppressBuiltInModel` | Stale upstream model suppression plus optional user-facing error hint | Provider needs to hide stale upstream rows or replace them with a vendor hint |
|
||||
| 31 | `suppressBuiltInModel` | Deprecated. Runtime hook is no longer called; use manifest `modelCatalog.suppressions` | Historical hook for hiding stale upstream rows; keep new suppression data in the plugin manifest |
|
||||
| 32 | `augmentModelCatalog` | Synthetic/final catalog rows appended after discovery | Provider needs synthetic forward-compat rows in `models list` and pickers |
|
||||
| 33 | `resolveThinkingProfile` | Model-specific `/think` level set, display labels, and default | Provider exposes a custom thinking ladder or binary label for selected models |
|
||||
| 34 | `isBinaryThinking` | On/off reasoning toggle compatibility hook | Provider exposes only binary thinking on/off |
|
||||
|
||||
@@ -735,11 +735,10 @@ Alias targets must be top-level providers owned by the same plugin. When a
|
||||
provider-filtered list uses an alias, OpenClaw can read the owning manifest and
|
||||
apply alias API/base URL overrides without loading provider runtime.
|
||||
|
||||
`suppressions` is the preferred static replacement for provider runtime
|
||||
`suppressBuiltInModel` hooks. Suppression entries are honored only when the
|
||||
provider is owned by the plugin or declared as a `modelCatalog.aliases` key that
|
||||
targets an owned provider. Runtime suppression hooks still run as deprecated
|
||||
compatibility fallback for plugins that have not migrated.
|
||||
`suppressions` replaces the old provider runtime `suppressBuiltInModel` hook.
|
||||
Suppression entries are honored only when the provider is owned by the plugin or
|
||||
declared as a `modelCatalog.aliases` key that targets an owned provider. Runtime
|
||||
suppression hooks are no longer called during model resolution.
|
||||
|
||||
Provider fields:
|
||||
|
||||
@@ -772,6 +771,16 @@ Model fields:
|
||||
| `replacedBy` | `string` | Replacement provider-local model id for deprecated rows. |
|
||||
| `tags` | `string[]` | Stable tags used by pickers and filters. |
|
||||
|
||||
Suppression fields:
|
||||
|
||||
| Field | Type | What it means |
|
||||
| -------------------------- | ---------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| `provider` | `string` | Provider id for the upstream row to suppress. Must be owned by this plugin or declared as an owned alias. |
|
||||
| `model` | `string` | Provider-local model id to suppress. |
|
||||
| `reason` | `string` | Optional message shown when the suppressed row is requested directly. |
|
||||
| `when.baseUrlHosts` | `string[]` | Optional list of effective provider base URL hosts required before the suppression applies. |
|
||||
| `when.providerConfigApiIn` | `string[]` | Optional list of exact provider-config `api` values required before the suppression applies. |
|
||||
|
||||
Do not put runtime-only data in `modelCatalog`. If a provider needs account
|
||||
state, an API request, or local process discovery to know the complete model
|
||||
set, declare that provider as `refreshable` or `runtime` in `discovery`.
|
||||
|
||||
@@ -452,7 +452,7 @@ API key auth, and dynamic model resolution.
|
||||
| 27 | `classifyFailoverReason` | Provider-owned rate-limit/overload classification |
|
||||
| 28 | `isCacheTtlEligible` | Prompt cache TTL gating |
|
||||
| 29 | `buildMissingAuthMessage` | Custom missing-auth hint |
|
||||
| 30 | `suppressBuiltInModel` | Hide stale upstream rows |
|
||||
| 30 | `suppressBuiltInModel` | Deprecated. Runtime hook is no longer called; use manifest `modelCatalog.suppressions` |
|
||||
| 31 | `augmentModelCatalog` | Synthetic forward-compat rows |
|
||||
| 32 | `resolveThinkingProfile` | Model-specific `/think` option set |
|
||||
| 33 | `isBinaryThinking` | Binary thinking on/off compatibility |
|
||||
|
||||
@@ -110,7 +110,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
|
||||
| `plugin-sdk/provider-env-vars` | Provider auth env-var lookup helpers |
|
||||
| `plugin-sdk/provider-auth` | `createProviderApiKeyAuthMethod`, `ensureApiKeyFromOptionEnvOrPrompt`, `upsertAuthProfile`, `upsertApiKeyProfile`, `writeOAuthCredentials` |
|
||||
| `plugin-sdk/provider-model-shared` | `ProviderReplayFamily`, `buildProviderReplayFamilyHooks`, `normalizeModelCompat`, shared replay-policy builders, provider-endpoint helpers, and model-id normalization helpers such as `normalizeNativeXaiModelId` |
|
||||
| `plugin-sdk/provider-catalog-runtime` | Provider catalog runtime hook and plugin-provider registry seams for contract tests |
|
||||
| `plugin-sdk/provider-catalog-runtime` | Provider catalog augmentation runtime hook and plugin-provider registry seams for contract tests |
|
||||
| `plugin-sdk/provider-catalog-shared` | `findCatalogTemplate`, `buildSingleProviderApiKeyCatalog`, `supportsNativeStreamingUsageCompat`, `applyProviderNativeStreamingUsageCompat` |
|
||||
| `plugin-sdk/provider-http` | Generic provider HTTP/endpoint capability helpers, provider HTTP errors, and audio transcription multipart form helpers |
|
||||
| `plugin-sdk/provider-web-fetch-contract` | Narrow web-fetch config/selection contract helpers such as `enablePluginInConfig` and `WebFetchProviderPlugin` |
|
||||
|
||||
@@ -67,6 +67,7 @@ async function getStatusWithEnvRefs(params: { appIdKey: string; appSecretKey: st
|
||||
|
||||
const feishuConfigure = createPluginSetupWizardConfigure(feishuPlugin);
|
||||
const feishuGetStatus = createPluginSetupWizardStatus(feishuPlugin);
|
||||
|
||||
describe("feishu setup wizard", () => {
|
||||
it("does not throw when config appId/appSecret are SecretRef objects", async () => {
|
||||
const text = vi
|
||||
|
||||
@@ -507,15 +507,6 @@ export function buildOpenAICodexProviderPlugin(): ProviderPlugin {
|
||||
],
|
||||
}),
|
||||
isModernModelRef: ({ modelId }) => matchesExactOrPrefix(modelId, OPENAI_CODEX_MODERN_MODEL_IDS),
|
||||
suppressBuiltInModel: ({ provider, modelId }) =>
|
||||
normalizeProviderId(provider) === PROVIDER_ID &&
|
||||
normalizeLowercaseStringOrEmpty(modelId) === "gpt-5.3-codex-spark"
|
||||
? {
|
||||
suppress: true,
|
||||
errorMessage:
|
||||
"gpt-5.3-codex-spark is no longer exposed by the OpenAI or Codex catalogs. Use openai/gpt-5.5.",
|
||||
}
|
||||
: undefined,
|
||||
preferRuntimeResolvedModel: (ctx) => {
|
||||
if (normalizeProviderId(ctx.provider) !== PROVIDER_ID) {
|
||||
return false;
|
||||
|
||||
@@ -77,8 +77,6 @@ const OPENAI_MODERN_MODEL_IDS = [
|
||||
OPENAI_GPT_54_NANO_MODEL_ID,
|
||||
"gpt-5.2",
|
||||
] as const;
|
||||
const OPENAI_DIRECT_SPARK_MODEL_ID = "gpt-5.3-codex-spark";
|
||||
const SUPPRESSED_SPARK_PROVIDERS = new Set(["openai", "azure-openai-responses"]);
|
||||
function shouldUseOpenAIResponsesTransport(params: {
|
||||
provider: string;
|
||||
api?: string | null;
|
||||
@@ -260,18 +258,6 @@ export function buildOpenAIProvider(): ProviderPlugin {
|
||||
}
|
||||
return 'No API key found for provider "openai". You are authenticated with OpenAI Codex OAuth. Use openai-codex/gpt-5.5, or set OPENAI_API_KEY for direct OpenAI API access.';
|
||||
},
|
||||
suppressBuiltInModel: (ctx) => {
|
||||
if (
|
||||
!SUPPRESSED_SPARK_PROVIDERS.has(normalizeProviderId(ctx.provider)) ||
|
||||
normalizeLowercaseStringOrEmpty(ctx.modelId) !== OPENAI_DIRECT_SPARK_MODEL_ID
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
suppress: true,
|
||||
errorMessage: `Unknown model: ${ctx.provider}/${OPENAI_DIRECT_SPARK_MODEL_ID}. ${OPENAI_DIRECT_SPARK_MODEL_ID} is no longer exposed by the OpenAI or Codex catalogs. Use openai/gpt-5.5.`,
|
||||
};
|
||||
},
|
||||
augmentModelCatalog: (ctx) => {
|
||||
const openAiGpt55ProTemplate = findCatalogTemplate({
|
||||
entries: ctx.entries,
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
import {
|
||||
expectAugmentedCodexCatalog,
|
||||
expectedAugmentedOpenaiCodexCatalogEntriesWithGpt55,
|
||||
expectCodexBuiltInSuppression,
|
||||
expectCodexMissingAuthHint,
|
||||
importProviderRuntimeCatalogModule,
|
||||
loadBundledPluginPublicSurface,
|
||||
@@ -49,17 +48,6 @@ vi.mock("openclaw/plugin-sdk/provider-catalog-runtime", async () => {
|
||||
}
|
||||
return supplemental;
|
||||
},
|
||||
resolveProviderBuiltInModelSuppression: (params: {
|
||||
context: Parameters<NonNullable<ProviderPlugin["suppressBuiltInModel"]>>[0];
|
||||
}) => {
|
||||
for (const provider of resolveCatalogHookProviders(params)) {
|
||||
const result = provider.suppressBuiltInModel?.(params.context);
|
||||
if (result?.suppress) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
resolveOwningPluginIdsForProvider: (params: unknown) =>
|
||||
resolveOwningPluginIdsForProviderMock(params as never),
|
||||
resolveCatalogHookProviderPluginIds: (params: unknown) =>
|
||||
@@ -86,15 +74,11 @@ export function describeOpenAIProviderCatalogContract() {
|
||||
})
|
||||
).providers;
|
||||
const openaiProvider = requireRegisteredProvider(openaiProviders, "openai", "provider");
|
||||
const {
|
||||
augmentModelCatalogWithProviderPlugins,
|
||||
resetProviderRuntimeHookCacheForTest,
|
||||
resolveProviderBuiltInModelSuppression,
|
||||
} = await importProviderRuntimeCatalogModule();
|
||||
const { augmentModelCatalogWithProviderPlugins, resetProviderRuntimeHookCacheForTest } =
|
||||
await importProviderRuntimeCatalogModule();
|
||||
return {
|
||||
augmentModelCatalogWithProviderPlugins,
|
||||
resetProviderRuntimeHookCacheForTest,
|
||||
resolveProviderBuiltInModelSuppression,
|
||||
openaiProviders,
|
||||
openaiProvider,
|
||||
};
|
||||
@@ -141,11 +125,6 @@ export function describeOpenAIProviderCatalogContract() {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps built-in model suppression wired through the provider runtime", async () => {
|
||||
const { resolveProviderBuiltInModelSuppression } = await contractDepsPromise;
|
||||
expectCodexBuiltInSuppression(resolveProviderBuiltInModelSuppression);
|
||||
});
|
||||
|
||||
it("keeps bundled model augmentation wired through the provider runtime", async () => {
|
||||
const { augmentModelCatalogWithProviderPlugins } = await contractDepsPromise;
|
||||
await expectAugmentedCodexCatalog(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import qwenPlugin from "./index.js";
|
||||
@@ -8,42 +7,9 @@ async function registerQwenProvider() {
|
||||
}
|
||||
|
||||
describe("qwen provider plugin", () => {
|
||||
it("does not suppress exact custom modelstudio providers owned by another api", async () => {
|
||||
const provider = await registerQwenProvider();
|
||||
const config = {
|
||||
models: {
|
||||
providers: {
|
||||
modelstudio: {
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
|
||||
models: [{ id: "qwen3.6-plus", name: "Qwen 3.6 Plus" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
provider.suppressBuiltInModel?.({
|
||||
config,
|
||||
env: {},
|
||||
provider: "modelstudio",
|
||||
modelId: "qwen3.6-plus",
|
||||
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("still suppresses legacy modelstudio refs on Qwen Coding Plan endpoints", async () => {
|
||||
it("does not expose runtime model suppression hooks", async () => {
|
||||
const provider = await registerQwenProvider();
|
||||
|
||||
expect(
|
||||
provider.suppressBuiltInModel?.({
|
||||
config: {},
|
||||
env: {},
|
||||
provider: "modelstudio",
|
||||
modelId: "qwen3.6-plus",
|
||||
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
|
||||
})?.suppress,
|
||||
).toBe(true);
|
||||
expect(provider.suppressBuiltInModel).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,29 +40,6 @@ function resolveConfiguredQwenBaseUrl(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isQwen36PlusUnsupportedForConfig(params: {
|
||||
config: Parameters<typeof resolveConfiguredQwenBaseUrl>[0];
|
||||
baseUrl?: string;
|
||||
}): boolean {
|
||||
return isQwenCodingPlanBaseUrl(params.baseUrl ?? resolveConfiguredQwenBaseUrl(params.config));
|
||||
}
|
||||
|
||||
function hasExactForeignApiOwner(params: {
|
||||
provider: string;
|
||||
config: { models?: { providers?: Record<string, { api?: string } | undefined> } } | undefined;
|
||||
}): boolean {
|
||||
const providers = params.config?.models?.providers;
|
||||
if (!providers) {
|
||||
return false;
|
||||
}
|
||||
const provider = normalizeProviderId(params.provider);
|
||||
const exact = Object.entries(providers).find(
|
||||
([providerId]) => normalizeProviderId(providerId) === provider,
|
||||
)?.[1];
|
||||
const api = normalizeProviderId(exact?.api ?? "");
|
||||
return !!api && api !== PROVIDER_ID && api !== LEGACY_PROVIDER_ID;
|
||||
}
|
||||
|
||||
export default defineSingleProviderPluginEntry({
|
||||
id: PROVIDER_ID,
|
||||
name: "Qwen Provider",
|
||||
@@ -192,22 +169,6 @@ export default defineSingleProviderPluginEntry({
|
||||
? { ...providerConfig, models }
|
||||
: undefined;
|
||||
},
|
||||
suppressBuiltInModel: (ctx) => {
|
||||
const provider = normalizeProviderId(ctx.provider);
|
||||
if (
|
||||
(provider !== PROVIDER_ID && provider !== LEGACY_PROVIDER_ID) ||
|
||||
hasExactForeignApiOwner({ provider: ctx.provider, config: ctx.config }) ||
|
||||
ctx.modelId !== QWEN_36_PLUS_MODEL_ID ||
|
||||
!isQwen36PlusUnsupportedForConfig({ config: ctx.config, baseUrl: ctx.baseUrl })
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
suppress: true,
|
||||
errorMessage:
|
||||
"Unknown model: qwen/qwen3.6-plus. qwen3.6-plus is not supported on the Qwen Coding Plan endpoint; use a Standard pay-as-you-go Qwen endpoint or choose qwen/qwen3.5-plus.",
|
||||
};
|
||||
},
|
||||
},
|
||||
register(api) {
|
||||
api.registerMediaUnderstandingProvider(buildQwenMediaUnderstandingProvider());
|
||||
|
||||
@@ -29,6 +29,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelCatalog": {
|
||||
"suppressions": [
|
||||
{
|
||||
"provider": "qwen",
|
||||
"model": "qwen3.6-plus",
|
||||
"reason": "qwen3.6-plus is not supported on the Qwen Coding Plan endpoint; use a Standard pay-as-you-go Qwen endpoint or choose qwen/qwen3.5-plus.",
|
||||
"when": {
|
||||
"baseUrlHosts": ["coding.dashscope.aliyuncs.com", "coding-intl.dashscope.aliyuncs.com"],
|
||||
"providerConfigApiIn": ["qwen", "modelstudio"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"provider": "modelstudio",
|
||||
"model": "qwen3.6-plus",
|
||||
"reason": "qwen3.6-plus is not supported on the Qwen Coding Plan endpoint; use a Standard pay-as-you-go Qwen endpoint or choose qwen/qwen3.5-plus.",
|
||||
"when": {
|
||||
"baseUrlHosts": ["coding.dashscope.aliyuncs.com", "coding-intl.dashscope.aliyuncs.com"],
|
||||
"providerConfigApiIn": ["qwen", "modelstudio"]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"contracts": {
|
||||
"mediaUnderstandingProviders": ["qwen"],
|
||||
"videoGenerationProviders": ["qwen"]
|
||||
|
||||
@@ -2,26 +2,20 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
resolveManifestBuiltInModelSuppression: vi.fn(),
|
||||
resolveProviderBuiltInModelSuppression: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/manifest-model-suppression.js", () => ({
|
||||
resolveManifestBuiltInModelSuppression: mocks.resolveManifestBuiltInModelSuppression,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/provider-runtime.js", () => ({
|
||||
resolveProviderBuiltInModelSuppression: mocks.resolveProviderBuiltInModelSuppression,
|
||||
}));
|
||||
|
||||
import { shouldSuppressBuiltInModel } from "./model-suppression.js";
|
||||
|
||||
describe("model suppression", () => {
|
||||
beforeEach(() => {
|
||||
mocks.resolveManifestBuiltInModelSuppression.mockReset();
|
||||
mocks.resolveProviderBuiltInModelSuppression.mockReset();
|
||||
});
|
||||
|
||||
it("uses manifest suppression before runtime hooks", () => {
|
||||
it("uses manifest suppression", () => {
|
||||
mocks.resolveManifestBuiltInModelSuppression.mockReturnValueOnce({
|
||||
suppress: true,
|
||||
errorMessage: "manifest suppression",
|
||||
@@ -35,23 +29,18 @@ describe("model suppression", () => {
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(mocks.resolveProviderBuiltInModelSuppression).not.toHaveBeenCalled();
|
||||
expect(mocks.resolveManifestBuiltInModelSuppression).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("falls back to runtime hooks when no manifest suppression matches", () => {
|
||||
mocks.resolveProviderBuiltInModelSuppression.mockReturnValueOnce({
|
||||
suppress: true,
|
||||
errorMessage: "runtime suppression",
|
||||
});
|
||||
|
||||
it("does not run deprecated runtime suppression hooks", () => {
|
||||
expect(
|
||||
shouldSuppressBuiltInModel({
|
||||
provider: "openai",
|
||||
id: "gpt-5.3-codex-spark",
|
||||
config: {},
|
||||
}),
|
||||
).toBe(true);
|
||||
).toBe(false);
|
||||
|
||||
expect(mocks.resolveProviderBuiltInModelSuppression).toHaveBeenCalledOnce();
|
||||
expect(mocks.resolveManifestBuiltInModelSuppression).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveManifestBuiltInModelSuppression } from "../plugins/manifest-model-suppression.js";
|
||||
import { resolveProviderBuiltInModelSuppression } from "../plugins/provider-runtime.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { normalizeProviderId } from "./provider-id.js";
|
||||
|
||||
function resolveBuiltInModelSuppressionFromManifest(params: {
|
||||
provider?: string | null;
|
||||
id?: string | null;
|
||||
baseUrl?: string | null;
|
||||
config?: OpenClawConfig;
|
||||
}) {
|
||||
const provider = normalizeProviderId(params.provider ?? "");
|
||||
@@ -18,6 +18,7 @@ function resolveBuiltInModelSuppressionFromManifest(params: {
|
||||
provider,
|
||||
id: modelId,
|
||||
...(params.config ? { config: params.config } : {}),
|
||||
...(params.baseUrl ? { baseUrl: params.baseUrl } : {}),
|
||||
env: process.env,
|
||||
});
|
||||
}
|
||||
@@ -37,17 +38,7 @@ function resolveBuiltInModelSuppression(params: {
|
||||
if (!provider || !modelId) {
|
||||
return undefined;
|
||||
}
|
||||
return resolveProviderBuiltInModelSuppression({
|
||||
...(params.config ? { config: params.config } : {}),
|
||||
env: process.env,
|
||||
context: {
|
||||
...(params.config ? { config: params.config } : {}),
|
||||
env: process.env,
|
||||
provider,
|
||||
modelId,
|
||||
...(params.baseUrl ? { baseUrl: params.baseUrl } : {}),
|
||||
},
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function shouldSuppressBuiltInModelFromManifest(params: {
|
||||
|
||||
@@ -255,7 +255,10 @@ export async function ensureOpenClawModelsJson(
|
||||
const refreshedCacheKey = modelsJsonReadyCacheKey(targetPath, refreshedFingerprint);
|
||||
if (refreshedCacheKey !== cacheKey) {
|
||||
MODELS_JSON_STATE.readyCache.delete(cacheKey);
|
||||
MODELS_JSON_STATE.readyCache.set(refreshedCacheKey, Promise.resolve(settled));
|
||||
MODELS_JSON_STATE.readyCache.set(
|
||||
refreshedCacheKey,
|
||||
Promise.resolve({ fingerprint: refreshedFingerprint, result: settled.result }),
|
||||
);
|
||||
}
|
||||
return settled.result;
|
||||
} catch (error) {
|
||||
|
||||
@@ -14,7 +14,6 @@ vi.mock("../../plugins/provider-runtime.js", () => ({
|
||||
normalizeProviderResolvedModelWithPlugin: () => undefined,
|
||||
normalizeProviderTransportWithPlugin: () => undefined,
|
||||
prepareProviderDynamicModel: async () => undefined,
|
||||
resolveProviderBuiltInModelSuppression: () => undefined,
|
||||
runProviderDynamicModel: () => undefined,
|
||||
shouldPreferProviderRuntimeResolvedModel: () => false,
|
||||
}));
|
||||
|
||||
@@ -42,7 +42,6 @@ vi.mock("../../plugins/provider-runtime.js", () => ({
|
||||
normalizeProviderResolvedModelWithPlugin: () => undefined,
|
||||
normalizeProviderTransportWithPlugin: () => undefined,
|
||||
prepareProviderDynamicModel: async () => {},
|
||||
resolveProviderBuiltInModelSuppression: () => undefined,
|
||||
runProviderDynamicModel: () => undefined,
|
||||
shouldPreferProviderRuntimeResolvedModel: () => false,
|
||||
}));
|
||||
|
||||
@@ -147,7 +147,6 @@ const providerRuntimeMocks = vi.hoisted(() => ({
|
||||
refreshProviderOAuthCredentialWithPlugin: vi.fn(async () => undefined),
|
||||
resetProviderRuntimeHookCacheForTest: vi.fn(() => {}),
|
||||
resolveProviderBinaryThinking: vi.fn(() => undefined),
|
||||
resolveProviderBuiltInModelSuppression: vi.fn(() => undefined),
|
||||
resolveProviderCacheTtlEligibility: vi.fn(() => undefined),
|
||||
resolveProviderCapabilitiesWithPlugin: vi.fn(() => undefined),
|
||||
resolveProviderDefaultThinkingLevel: vi.fn(() => undefined),
|
||||
|
||||
@@ -212,6 +212,9 @@ describe("manifest model catalog suppression planner", () => {
|
||||
provider: "openai",
|
||||
model: "gpt-5.3-codex-spark",
|
||||
reason: "Use openai/gpt-5.5.",
|
||||
when: {
|
||||
baseUrlHosts: ["api.openai.com"],
|
||||
},
|
||||
},
|
||||
{
|
||||
provider: "azure-openai-responses",
|
||||
@@ -243,6 +246,9 @@ describe("manifest model catalog suppression planner", () => {
|
||||
model: "gpt-5.3-codex-spark",
|
||||
mergeKey: "openai::gpt-5.3-codex-spark",
|
||||
reason: "Use openai/gpt-5.5.",
|
||||
when: {
|
||||
baseUrlHosts: ["api.openai.com"],
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -46,6 +46,7 @@ export type ManifestModelCatalogSuppressionEntry = {
|
||||
model: string;
|
||||
mergeKey: string;
|
||||
reason?: string;
|
||||
when?: NonNullable<ModelCatalog["suppressions"]>[number]["when"];
|
||||
};
|
||||
|
||||
export type ManifestModelCatalogSuppressionPlan = {
|
||||
@@ -239,6 +240,7 @@ export function planManifestModelCatalogSuppressions(params: {
|
||||
model,
|
||||
mergeKey: buildModelCatalogMergeKey(provider, model),
|
||||
...(suppression.reason ? { reason: suppression.reason } : {}),
|
||||
...(suppression.when ? { when: suppression.when } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,10 @@ describe("model catalog normalization", () => {
|
||||
provider: "Azure-OpenAI-Responses",
|
||||
model: "gpt-5.3-codex-spark",
|
||||
reason: "not available",
|
||||
when: {
|
||||
baseUrlHosts: ["CODING-INTL.DASHSCOPE.ALIYUNCS.COM"],
|
||||
providerConfigApiIn: ["Qwen", "ModelStudio"],
|
||||
},
|
||||
},
|
||||
],
|
||||
discovery: {
|
||||
@@ -154,6 +158,10 @@ describe("model catalog normalization", () => {
|
||||
provider: "azure-openai-responses",
|
||||
model: "gpt-5.3-codex-spark",
|
||||
reason: "not available",
|
||||
when: {
|
||||
baseUrlHosts: ["coding-intl.dashscope.aliyuncs.com"],
|
||||
providerConfigApiIn: ["qwen", "modelstudio"],
|
||||
},
|
||||
},
|
||||
],
|
||||
discovery: {
|
||||
|
||||
@@ -370,10 +370,25 @@ function normalizeModelCatalogSuppressions(value: unknown): ModelCatalogSuppress
|
||||
continue;
|
||||
}
|
||||
const reason = normalizeOptionalString(entry.reason) ?? "";
|
||||
const rawWhen = isRecord(entry.when) ? entry.when : undefined;
|
||||
const baseUrlHosts = normalizeTrimmedStringList(rawWhen?.baseUrlHosts).map((host) =>
|
||||
host.toLowerCase(),
|
||||
);
|
||||
const providerConfigApiIn = normalizeTrimmedStringList(rawWhen?.providerConfigApiIn).map(
|
||||
(api) => api.toLowerCase(),
|
||||
);
|
||||
const when =
|
||||
baseUrlHosts.length > 0 || providerConfigApiIn.length > 0
|
||||
? {
|
||||
...(baseUrlHosts.length > 0 ? { baseUrlHosts } : {}),
|
||||
...(providerConfigApiIn.length > 0 ? { providerConfigApiIn } : {}),
|
||||
}
|
||||
: undefined;
|
||||
suppressions.push({
|
||||
provider,
|
||||
model,
|
||||
...(reason ? { reason } : {}),
|
||||
...(when ? { when } : {}),
|
||||
});
|
||||
}
|
||||
return suppressions.length > 0 ? suppressions : undefined;
|
||||
|
||||
@@ -63,6 +63,10 @@ export type ModelCatalogSuppression = {
|
||||
provider: string;
|
||||
model: string;
|
||||
reason?: string;
|
||||
when?: {
|
||||
baseUrlHosts?: string[];
|
||||
providerConfigApiIn?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export type ModelCatalog = {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
export {
|
||||
augmentModelCatalogWithProviderPlugins,
|
||||
resetProviderRuntimeHookCacheForTest,
|
||||
resolveProviderBuiltInModelSuppression,
|
||||
} from "../plugins/provider-runtime.js";
|
||||
export {
|
||||
resolveCatalogHookProviderPluginIds,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export {
|
||||
expectAugmentedCodexCatalog,
|
||||
expectedAugmentedOpenaiCodexCatalogEntriesWithGpt55,
|
||||
expectCodexBuiltInSuppression,
|
||||
expectCodexMissingAuthHint,
|
||||
} from "../testing.js";
|
||||
export type { ProviderPlugin } from "../provider-model-shared.js";
|
||||
@@ -12,20 +11,14 @@ export {
|
||||
|
||||
type ProviderRuntimeCatalogModule = Pick<
|
||||
typeof import("openclaw/plugin-sdk/provider-catalog-runtime"),
|
||||
| "augmentModelCatalogWithProviderPlugins"
|
||||
| "resetProviderRuntimeHookCacheForTest"
|
||||
| "resolveProviderBuiltInModelSuppression"
|
||||
"augmentModelCatalogWithProviderPlugins" | "resetProviderRuntimeHookCacheForTest"
|
||||
>;
|
||||
|
||||
export async function importProviderRuntimeCatalogModule(): Promise<ProviderRuntimeCatalogModule> {
|
||||
const {
|
||||
augmentModelCatalogWithProviderPlugins,
|
||||
resetProviderRuntimeHookCacheForTest,
|
||||
resolveProviderBuiltInModelSuppression,
|
||||
} = await import("openclaw/plugin-sdk/provider-catalog-runtime");
|
||||
const { augmentModelCatalogWithProviderPlugins, resetProviderRuntimeHookCacheForTest } =
|
||||
await import("openclaw/plugin-sdk/provider-catalog-runtime");
|
||||
return {
|
||||
augmentModelCatalogWithProviderPlugins,
|
||||
resetProviderRuntimeHookCacheForTest,
|
||||
resolveProviderBuiltInModelSuppression,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,4 +88,92 @@ describe("manifest model suppression", () => {
|
||||
|
||||
expect(mocks.loadPluginManifestRegistryForPluginRegistry).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("matches conditional suppressions by base URL host", () => {
|
||||
mocks.loadPluginManifestRegistryForPluginRegistry.mockReturnValue({
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
{
|
||||
id: "qwen",
|
||||
providers: ["qwen", "modelstudio"],
|
||||
modelCatalog: {
|
||||
suppressions: [
|
||||
{
|
||||
provider: "qwen",
|
||||
model: "qwen3.6-plus",
|
||||
reason: "Use qwen/qwen3.5-plus.",
|
||||
when: {
|
||||
baseUrlHosts: [
|
||||
"coding.dashscope.aliyuncs.com",
|
||||
"coding-intl.dashscope.aliyuncs.com",
|
||||
],
|
||||
providerConfigApiIn: ["qwen", "modelstudio"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveManifestBuiltInModelSuppression({
|
||||
provider: "qwen",
|
||||
id: "qwen3.6-plus",
|
||||
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
|
||||
env: process.env,
|
||||
})?.suppress,
|
||||
).toBe(true);
|
||||
expect(
|
||||
resolveManifestBuiltInModelSuppression({
|
||||
provider: "qwen",
|
||||
id: "qwen3.6-plus",
|
||||
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
env: process.env,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not apply conditional suppressions to custom providers with a foreign api owner", () => {
|
||||
mocks.loadPluginManifestRegistryForPluginRegistry.mockReturnValue({
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
{
|
||||
id: "qwen",
|
||||
providers: ["modelstudio"],
|
||||
modelCatalog: {
|
||||
suppressions: [
|
||||
{
|
||||
provider: "modelstudio",
|
||||
model: "qwen3.6-plus",
|
||||
when: {
|
||||
baseUrlHosts: ["coding-intl.dashscope.aliyuncs.com"],
|
||||
providerConfigApiIn: ["qwen", "modelstudio"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveManifestBuiltInModelSuppression({
|
||||
provider: "modelstudio",
|
||||
id: "qwen3.6-plus",
|
||||
config: {
|
||||
models: {
|
||||
providers: {
|
||||
modelstudio: {
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: process.env,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,6 +77,70 @@ function buildManifestSuppressionError(params: {
|
||||
return params.reason ? `Unknown model: ${ref}. ${params.reason}` : `Unknown model: ${ref}.`;
|
||||
}
|
||||
|
||||
function normalizeBaseUrlHost(baseUrl: string | null | undefined): string {
|
||||
if (!baseUrl?.trim()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return new URL(baseUrl).hostname.toLowerCase();
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function resolveConfiguredProviderValue(params: {
|
||||
provider: string;
|
||||
config?: OpenClawConfig;
|
||||
}): { api?: string; baseUrl?: string } | undefined {
|
||||
const providers = params.config?.models?.providers;
|
||||
if (!providers) {
|
||||
return undefined;
|
||||
}
|
||||
for (const [providerId, entry] of Object.entries(providers)) {
|
||||
if (normalizeLowercaseStringOrEmpty(providerId) !== params.provider) {
|
||||
continue;
|
||||
}
|
||||
return {
|
||||
api: normalizeLowercaseStringOrEmpty(entry?.api),
|
||||
baseUrl: typeof entry?.baseUrl === "string" ? entry.baseUrl : undefined,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function manifestSuppressionMatchesConditions(params: {
|
||||
suppression: ManifestModelCatalogSuppressionEntry;
|
||||
provider: string;
|
||||
baseUrl?: string | null;
|
||||
config?: OpenClawConfig;
|
||||
}): boolean {
|
||||
const when = params.suppression.when;
|
||||
if (!when) {
|
||||
return true;
|
||||
}
|
||||
const configuredProvider = resolveConfiguredProviderValue({
|
||||
provider: params.provider,
|
||||
config: params.config,
|
||||
});
|
||||
if (when.providerConfigApiIn?.length && configuredProvider?.api) {
|
||||
const allowedApis = new Set(when.providerConfigApiIn.map(normalizeLowercaseStringOrEmpty));
|
||||
if (!allowedApis.has(configuredProvider.api)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (when.baseUrlHosts?.length) {
|
||||
const baseUrlHost = normalizeBaseUrlHost(params.baseUrl ?? configuredProvider?.baseUrl);
|
||||
if (!baseUrlHost) {
|
||||
return false;
|
||||
}
|
||||
const allowedHosts = new Set(when.baseUrlHosts.map(normalizeLowercaseStringOrEmpty));
|
||||
if (!allowedHosts.has(baseUrlHost)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function clearManifestModelSuppressionCacheForTest(): void {
|
||||
cacheWithoutConfig = new WeakMap<NodeJS.ProcessEnv, ManifestSuppressionCache>();
|
||||
cacheByConfig = new WeakMap<
|
||||
@@ -91,6 +155,7 @@ export function resolveManifestBuiltInModelSuppression(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
baseUrl?: string | null;
|
||||
}) {
|
||||
const provider = normalizeLowercaseStringOrEmpty(params.provider);
|
||||
const modelId = normalizeLowercaseStringOrEmpty(params.id);
|
||||
@@ -102,7 +167,16 @@ export function resolveManifestBuiltInModelSuppression(params: {
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env ?? process.env,
|
||||
}).find((entry) => entry.mergeKey === mergeKey);
|
||||
}).find(
|
||||
(entry) =>
|
||||
entry.mergeKey === mergeKey &&
|
||||
manifestSuppressionMatchesConditions({
|
||||
suppression: entry,
|
||||
provider,
|
||||
baseUrl: params.baseUrl,
|
||||
config: params.config,
|
||||
}),
|
||||
);
|
||||
if (!suppression) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { normalizePluginIdScope, serializePluginIdScope } from "./plugin-scope.js";
|
||||
import { resolveProviderConfigApiOwnerHint } from "./provider-config-owner.js";
|
||||
import { resolveOwningPluginIdsForProvider } from "./providers.js";
|
||||
import { isPluginProvidersLoadInFlight, resolvePluginProviders } from "./providers.runtime.js";
|
||||
import { resolvePluginCacheInputs } from "./roots.js";
|
||||
import { getActivePluginRegistryWorkspaceDirFromState } from "./runtime-state.js";
|
||||
@@ -45,12 +46,100 @@ function resolveHookProviderCacheBucket(env: NodeJS.ProcessEnv) {
|
||||
return bucket;
|
||||
}
|
||||
|
||||
function resolveHookProviderConfigCacheShape(config: OpenClawConfig | undefined): unknown {
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function projectPluginEntryForProviderHookCache(
|
||||
pluginId: string,
|
||||
entry: unknown,
|
||||
fullConfigPluginIds: ReadonlySet<string>,
|
||||
): unknown {
|
||||
if (!isRecord(entry) || fullConfigPluginIds.has(pluginId)) {
|
||||
return entry;
|
||||
}
|
||||
const {
|
||||
config: _config,
|
||||
hooks: _hooks,
|
||||
subagent: _subagent,
|
||||
apiKey: _apiKey,
|
||||
env: _env,
|
||||
...rest
|
||||
} = entry;
|
||||
return rest;
|
||||
}
|
||||
|
||||
function projectPluginsConfigForProviderHookCache(
|
||||
plugins: OpenClawConfig["plugins"],
|
||||
fullConfigPluginIds: ReadonlySet<string>,
|
||||
): unknown {
|
||||
if (!isRecord(plugins)) {
|
||||
return plugins ?? null;
|
||||
}
|
||||
const entries = isRecord(plugins.entries)
|
||||
? Object.fromEntries(
|
||||
Object.entries(plugins.entries)
|
||||
.toSorted(([left], [right]) => left.localeCompare(right))
|
||||
.map(([pluginId, entry]) => [
|
||||
pluginId,
|
||||
projectPluginEntryForProviderHookCache(pluginId, entry, fullConfigPluginIds),
|
||||
]),
|
||||
)
|
||||
: plugins.entries;
|
||||
return {
|
||||
...plugins,
|
||||
entries,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveProviderOwnerConfigPluginIds(params: {
|
||||
providerRefs?: readonly string[];
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): string[] {
|
||||
if (!params.providerRefs?.length) {
|
||||
return [];
|
||||
}
|
||||
const pluginIds = new Set<string>();
|
||||
for (const provider of params.providerRefs) {
|
||||
for (const pluginId of resolveOwningPluginIdsForProvider({
|
||||
provider,
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
}) ?? []) {
|
||||
pluginIds.add(pluginId);
|
||||
}
|
||||
const apiOwnerHint = resolveProviderConfigApiOwnerHint({
|
||||
provider,
|
||||
config: params.config,
|
||||
});
|
||||
if (!apiOwnerHint) {
|
||||
continue;
|
||||
}
|
||||
for (const pluginId of resolveOwningPluginIdsForProvider({
|
||||
provider: apiOwnerHint,
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
}) ?? []) {
|
||||
pluginIds.add(pluginId);
|
||||
}
|
||||
}
|
||||
return [...pluginIds].toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function resolveProviderHookConfigCacheShape(
|
||||
config: OpenClawConfig | undefined,
|
||||
fullConfigPluginIds: readonly string[] | undefined,
|
||||
): unknown {
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
const fullConfigPluginIdSet = new Set(fullConfigPluginIds ?? []);
|
||||
return {
|
||||
plugins: config.plugins,
|
||||
plugins: projectPluginsConfigForProviderHookCache(config.plugins, fullConfigPluginIdSet),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,13 +149,24 @@ function buildHookProviderCacheKey(params: {
|
||||
onlyPluginIds?: string[];
|
||||
providerRefs?: string[];
|
||||
env?: NodeJS.ProcessEnv;
|
||||
fullConfigPluginIds?: string[];
|
||||
applyAutoEnable?: boolean;
|
||||
bundledProviderAllowlistCompat?: boolean;
|
||||
bundledProviderVitestCompat?: boolean;
|
||||
installBundledRuntimeDeps?: boolean;
|
||||
}) {
|
||||
const { roots } = resolvePluginCacheInputs({
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
const onlyPluginIds = normalizePluginIdScope(params.onlyPluginIds);
|
||||
return `${roots.workspace ?? ""}::${roots.global}::${roots.stock ?? ""}::${JSON.stringify(resolveHookProviderConfigCacheShape(params.config))}::${serializePluginIdScope(onlyPluginIds)}::${JSON.stringify(params.providerRefs ?? [])}`;
|
||||
const loadPolicy = {
|
||||
applyAutoEnable: params.applyAutoEnable ?? true,
|
||||
bundledProviderAllowlistCompat: params.bundledProviderAllowlistCompat ?? true,
|
||||
bundledProviderVitestCompat: params.bundledProviderVitestCompat ?? true,
|
||||
installBundledRuntimeDeps: params.installBundledRuntimeDeps ?? false,
|
||||
};
|
||||
return `${roots.workspace ?? ""}::${roots.global}::${roots.stock ?? ""}::${JSON.stringify(resolveProviderHookConfigCacheShape(params.config, params.fullConfigPluginIds))}::${serializePluginIdScope(onlyPluginIds)}::${JSON.stringify(params.providerRefs ?? [])}::${JSON.stringify(loadPolicy)}`;
|
||||
}
|
||||
|
||||
export function clearProviderRuntimeHookCache(): void {
|
||||
@@ -95,12 +195,30 @@ export function resolveProviderPluginsForHooks(params: {
|
||||
const env = params.env ?? process.env;
|
||||
const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState();
|
||||
const cacheBucket = resolveHookProviderCacheBucket(env);
|
||||
const onlyPluginIds = normalizePluginIdScope(params.onlyPluginIds);
|
||||
const explicitPluginIds = onlyPluginIds ?? [];
|
||||
const fullConfigPluginIds = [
|
||||
...new Set([
|
||||
...explicitPluginIds,
|
||||
...resolveProviderOwnerConfigPluginIds({
|
||||
providerRefs: params.providerRefs,
|
||||
config: params.config,
|
||||
workspaceDir,
|
||||
env,
|
||||
}),
|
||||
]),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
const cacheKey = buildHookProviderCacheKey({
|
||||
config: params.config,
|
||||
workspaceDir,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
onlyPluginIds,
|
||||
providerRefs: params.providerRefs,
|
||||
env,
|
||||
fullConfigPluginIds,
|
||||
applyAutoEnable: params.applyAutoEnable,
|
||||
bundledProviderAllowlistCompat: params.bundledProviderAllowlistCompat,
|
||||
bundledProviderVitestCompat: params.bundledProviderVitestCompat,
|
||||
installBundledRuntimeDeps: params.installBundledRuntimeDeps,
|
||||
});
|
||||
const cached = cacheBucket.get(cacheKey);
|
||||
if (cached) {
|
||||
|
||||
@@ -50,31 +50,6 @@ export function expectCodexMissingAuthHint(
|
||||
).toContain(expectedModel);
|
||||
}
|
||||
|
||||
export function expectCodexBuiltInSuppression(
|
||||
resolveProviderBuiltInModelSuppression: (params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
context: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
provider: string;
|
||||
modelId: string;
|
||||
};
|
||||
}) => unknown,
|
||||
) {
|
||||
expect(
|
||||
resolveProviderBuiltInModelSuppression({
|
||||
env: process.env,
|
||||
context: {
|
||||
env: process.env,
|
||||
provider: "azure-openai-responses",
|
||||
modelId: "gpt-5.3-codex-spark",
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
suppress: true,
|
||||
errorMessage: expect.stringContaining("gpt-5.3-codex-spark"),
|
||||
});
|
||||
}
|
||||
|
||||
export async function expectAugmentedCodexCatalog(
|
||||
augmentModelCatalogWithProviderPlugins: (params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { ModelProviderConfig, OpenClawConfig } from "../config/types.js";
|
||||
import type { ProviderRuntimeModel } from "./provider-runtime-model.types.js";
|
||||
import {
|
||||
expectAugmentedCodexCatalog,
|
||||
expectCodexBuiltInSuppression,
|
||||
expectCodexMissingAuthHint,
|
||||
expectedAugmentedOpenaiCodexCatalogEntries,
|
||||
} from "./provider-runtime.test-support.js";
|
||||
@@ -70,7 +69,6 @@ let resolveProviderFollowupFallbackRoute: typeof import("./provider-runtime.js")
|
||||
let resolveProviderStreamFn: typeof import("./provider-runtime.js").resolveProviderStreamFn;
|
||||
let resolveProviderCacheTtlEligibility: typeof import("./provider-runtime.js").resolveProviderCacheTtlEligibility;
|
||||
let resolveProviderBinaryThinking: typeof import("./provider-runtime.js").resolveProviderBinaryThinking;
|
||||
let resolveProviderBuiltInModelSuppression: typeof import("./provider-runtime.js").resolveProviderBuiltInModelSuppression;
|
||||
let createProviderEmbeddingProvider: typeof import("./provider-runtime.js").createProviderEmbeddingProvider;
|
||||
let resolveProviderDefaultThinkingLevel: typeof import("./provider-runtime.js").resolveProviderDefaultThinkingLevel;
|
||||
let resolveProviderModernModelRef: typeof import("./provider-runtime.js").resolveProviderModernModelRef;
|
||||
@@ -144,15 +142,6 @@ function createOpenAiCatalogProviderPlugin(
|
||||
id: "openai",
|
||||
label: "OpenAI",
|
||||
auth: [],
|
||||
suppressBuiltInModel: ({ provider, modelId }) =>
|
||||
(provider === "openai" || provider === "azure-openai-responses") &&
|
||||
modelId === "gpt-5.3-codex-spark"
|
||||
? {
|
||||
suppress: true,
|
||||
errorMessage:
|
||||
"gpt-5.3-codex-spark is no longer exposed by the OpenAI or Codex catalogs. Use openai/gpt-5.5.",
|
||||
}
|
||||
: undefined,
|
||||
augmentModelCatalog: () => [
|
||||
{ provider: "openai", id: "gpt-5.4", name: "gpt-5.4" },
|
||||
{ provider: "openai", id: "gpt-5.4-pro", name: "gpt-5.4-pro" },
|
||||
@@ -303,7 +292,6 @@ describe("provider-runtime", () => {
|
||||
resolveProviderStreamFn,
|
||||
resolveProviderCacheTtlEligibility,
|
||||
resolveProviderBinaryThinking,
|
||||
resolveProviderBuiltInModelSuppression,
|
||||
createProviderEmbeddingProvider,
|
||||
resolveProviderDefaultThinkingLevel,
|
||||
resolveProviderModernModelRef,
|
||||
@@ -405,6 +393,201 @@ describe("provider-runtime", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("separates provider hook cache keys by load policy", () => {
|
||||
const base = {
|
||||
workspaceDir: "/tmp/workspace",
|
||||
env: { OPENCLAW_HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv,
|
||||
providerRefs: ["demo"],
|
||||
};
|
||||
|
||||
expect(
|
||||
providerRuntimeTesting.buildHookProviderCacheKey({
|
||||
...base,
|
||||
applyAutoEnable: false,
|
||||
bundledProviderAllowlistCompat: false,
|
||||
bundledProviderVitestCompat: false,
|
||||
installBundledRuntimeDeps: false,
|
||||
}),
|
||||
).not.toBe(providerRuntimeTesting.buildHookProviderCacheKey(base));
|
||||
});
|
||||
|
||||
it("ignores unrelated plugin config values in provider hook cache keys", () => {
|
||||
const base = {
|
||||
workspaceDir: "/tmp/workspace",
|
||||
env: { OPENCLAW_HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv,
|
||||
onlyPluginIds: ["demo"],
|
||||
};
|
||||
const firstConfig = {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: { enabled: true, config: { endpoint: "https://demo.example" } },
|
||||
"active-memory": { enabled: true },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const secondConfig = {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: { enabled: true, config: { endpoint: "https://demo.example" } },
|
||||
"active-memory": { enabled: true, config: { qmd: { searchMode: "fast" } } },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
providerRuntimeTesting.buildHookProviderCacheKey({
|
||||
...base,
|
||||
config: firstConfig,
|
||||
fullConfigPluginIds: ["demo"],
|
||||
}),
|
||||
).toBe(
|
||||
providerRuntimeTesting.buildHookProviderCacheKey({
|
||||
...base,
|
||||
config: secondConfig,
|
||||
fullConfigPluginIds: ["demo"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps scoped provider plugin config in provider hook cache keys", () => {
|
||||
const base = {
|
||||
workspaceDir: "/tmp/workspace",
|
||||
env: { OPENCLAW_HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv,
|
||||
onlyPluginIds: ["demo"],
|
||||
fullConfigPluginIds: ["demo"],
|
||||
};
|
||||
|
||||
expect(
|
||||
providerRuntimeTesting.buildHookProviderCacheKey({
|
||||
...base,
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: { enabled: true, config: { endpoint: "https://one.example" } },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
}),
|
||||
).not.toBe(
|
||||
providerRuntimeTesting.buildHookProviderCacheKey({
|
||||
...base,
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: { enabled: true, config: { endpoint: "https://two.example" } },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps provider-ref owner plugin config in provider hook cache keys", () => {
|
||||
const provider: ProviderPlugin = {
|
||||
id: DEMO_PROVIDER_ID,
|
||||
label: "Demo",
|
||||
auth: [],
|
||||
};
|
||||
resolveOwningPluginIdsForProviderMock.mockReturnValue(["demo"]);
|
||||
resolvePluginProvidersMock.mockReturnValue([provider]);
|
||||
const firstConfig = {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: { enabled: true, config: { endpoint: "https://one.example" } },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const secondConfig = {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: { enabled: true, config: { endpoint: "https://two.example" } },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveProviderRuntimePlugin({ provider: DEMO_PROVIDER_ID, config: firstConfig })).toBe(
|
||||
provider,
|
||||
);
|
||||
expect(resolveProviderRuntimePlugin({ provider: DEMO_PROVIDER_ID, config: secondConfig })).toBe(
|
||||
provider,
|
||||
);
|
||||
|
||||
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("reuses provider-ref hook loads when unrelated plugin config changes", () => {
|
||||
const provider: ProviderPlugin = {
|
||||
id: DEMO_PROVIDER_ID,
|
||||
label: "Demo",
|
||||
auth: [],
|
||||
};
|
||||
resolveOwningPluginIdsForProviderMock.mockReturnValue(["demo"]);
|
||||
resolvePluginProvidersMock.mockReturnValue([provider]);
|
||||
const firstConfig = {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: { enabled: true, config: { endpoint: "https://demo.example" } },
|
||||
"active-memory": { enabled: true },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const secondConfig = {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: { enabled: true, config: { endpoint: "https://demo.example" } },
|
||||
"active-memory": { enabled: true, config: { qmd: { searchMode: "fast" } } },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveProviderRuntimePlugin({ provider: DEMO_PROVIDER_ID, config: firstConfig })).toBe(
|
||||
provider,
|
||||
);
|
||||
expect(resolveProviderRuntimePlugin({ provider: DEMO_PROVIDER_ID, config: secondConfig })).toBe(
|
||||
provider,
|
||||
);
|
||||
|
||||
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not reuse auto-enabled runtime providers for synthetic auth fallback", () => {
|
||||
const runtimeProvider: ProviderPlugin = {
|
||||
id: DEMO_PROVIDER_ID,
|
||||
label: "Demo",
|
||||
auth: [],
|
||||
resolveSyntheticAuth: () => ({
|
||||
apiKey: "default-runtime-token",
|
||||
source: "default runtime",
|
||||
mode: "api-key" as const,
|
||||
}),
|
||||
};
|
||||
resolvePluginProvidersMock.mockImplementation((params) =>
|
||||
params.applyAutoEnable === false &&
|
||||
params.bundledProviderAllowlistCompat === false &&
|
||||
params.bundledProviderVitestCompat === false &&
|
||||
params.installBundledRuntimeDeps === false
|
||||
? []
|
||||
: [runtimeProvider],
|
||||
);
|
||||
|
||||
expect(resolveProviderRuntimePlugin({ provider: DEMO_PROVIDER_ID })).toBe(runtimeProvider);
|
||||
|
||||
expect(
|
||||
resolveProviderSyntheticAuthWithPlugin({
|
||||
provider: DEMO_PROVIDER_ID,
|
||||
context: {
|
||||
provider: DEMO_PROVIDER_ID,
|
||||
providerConfig: {
|
||||
api: "ollama",
|
||||
baseUrl: "http://127.0.0.1:11434",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("skips provider runtime loading when no plugin declares external auth hooks", () => {
|
||||
expect(
|
||||
resolveExternalAuthProfilesWithPlugins({
|
||||
@@ -503,14 +686,14 @@ describe("provider-runtime", () => {
|
||||
expect(providerRuntimeWarnMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reuses catalog hook provider loads when only non-plugin config changes", () => {
|
||||
it("reuses catalog hook provider loads when only non-plugin config changes", async () => {
|
||||
resolveCatalogHookProviderPluginIdsMock.mockReturnValue(["demo"]);
|
||||
resolvePluginProvidersMock.mockReturnValue([
|
||||
{
|
||||
id: "demo",
|
||||
label: "Demo",
|
||||
auth: [],
|
||||
suppressBuiltInModel: () => ({ suppress: true, errorMessage: "suppressed" }),
|
||||
augmentModelCatalog: () => [{ provider: "demo", id: "demo-model", name: "Demo Model" }],
|
||||
},
|
||||
]);
|
||||
const baseConfig = {
|
||||
@@ -530,23 +713,64 @@ describe("provider-runtime", () => {
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
resolveProviderBuiltInModelSuppression({
|
||||
await augmentModelCatalogWithProviderPlugins({
|
||||
config: firstConfig,
|
||||
env: process.env,
|
||||
context: { config: firstConfig, env: process.env, provider: "openai", modelId: "demo" },
|
||||
})?.suppress,
|
||||
).toBe(true);
|
||||
context: { config: firstConfig, env: process.env, entries: [] },
|
||||
}),
|
||||
).toEqual([{ provider: "demo", id: "demo-model", name: "Demo Model" }]);
|
||||
expect(
|
||||
resolveProviderBuiltInModelSuppression({
|
||||
await augmentModelCatalogWithProviderPlugins({
|
||||
config: secondConfig,
|
||||
env: process.env,
|
||||
context: { config: secondConfig, env: process.env, provider: "openai", modelId: "demo" },
|
||||
})?.suppress,
|
||||
).toBe(true);
|
||||
context: { config: secondConfig, env: process.env, entries: [] },
|
||||
}),
|
||||
).toEqual([{ provider: "demo", id: "demo-model", name: "Demo Model" }]);
|
||||
|
||||
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("reuses catalog hook provider loads when unrelated plugin config changes", async () => {
|
||||
resolveCatalogHookProviderPluginIdsMock.mockReturnValue(["demo"]);
|
||||
resolvePluginProvidersMock.mockReturnValue([
|
||||
{
|
||||
id: "demo",
|
||||
label: "Demo",
|
||||
auth: [],
|
||||
augmentModelCatalog: () => [{ provider: "demo", id: "demo-model", name: "Demo Model" }],
|
||||
},
|
||||
]);
|
||||
const firstConfig = {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: { enabled: true, config: { endpoint: "https://demo.example" } },
|
||||
"active-memory": { enabled: true },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const secondConfig = {
|
||||
plugins: {
|
||||
entries: {
|
||||
demo: { enabled: true, config: { endpoint: "https://demo.example" } },
|
||||
"active-memory": { enabled: true, config: { qmd: { searchMode: "fast" } } },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
for (const config of [firstConfig, secondConfig]) {
|
||||
expect(
|
||||
await augmentModelCatalogWithProviderPlugins({
|
||||
config,
|
||||
env: process.env,
|
||||
context: { config, env: process.env, entries: [] },
|
||||
}),
|
||||
).toEqual([{ provider: "demo", id: "demo-model", name: "Demo Model" }]);
|
||||
}
|
||||
|
||||
expect(resolveCatalogHookProviderPluginIdsMock).toHaveBeenCalledTimes(1);
|
||||
expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns provider-prepared runtime auth for the matched provider", async () => {
|
||||
const prepareRuntimeAuth = vi.fn(async () => ({
|
||||
apiKey: "runtime-token",
|
||||
@@ -1653,7 +1877,6 @@ describe("provider-runtime", () => {
|
||||
]);
|
||||
|
||||
expectCodexMissingAuthHint(buildProviderMissingAuthMessageWithPlugin);
|
||||
expectCodexBuiltInSuppression(resolveProviderBuiltInModelSuppression);
|
||||
await expectAugmentedCodexCatalog(augmentModelCatalogWithProviderPlugins);
|
||||
|
||||
expectCalledOnce(
|
||||
@@ -1839,19 +2062,6 @@ describe("provider-runtime", () => {
|
||||
return [createOpenAiCatalogProviderPlugin()];
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveProviderBuiltInModelSuppression({
|
||||
env: process.env,
|
||||
context: {
|
||||
env: process.env,
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.3-codex-spark",
|
||||
},
|
||||
}),
|
||||
).toMatchObject({
|
||||
suppress: true,
|
||||
});
|
||||
|
||||
await expect(
|
||||
augmentModelCatalogWithProviderPlugins({
|
||||
env: process.env,
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
__testing as providerHookRuntimeTesting,
|
||||
clearProviderRuntimeHookCache as clearProviderHookRuntimeCache,
|
||||
prepareProviderExtraParams,
|
||||
resolveProviderHookConfigCacheShape,
|
||||
resolveProviderAuthProfileId,
|
||||
resolveProviderExtraParamsForTransport,
|
||||
resolveProviderFollowupFallbackRoute,
|
||||
@@ -43,7 +44,6 @@ import type {
|
||||
ProviderExternalAuthProfile,
|
||||
ProviderBuildMissingAuthMessageContext,
|
||||
ProviderBuildUnknownModelHintContext,
|
||||
ProviderBuiltInModelSuppressionContext,
|
||||
ProviderCacheTtlEligibilityContext,
|
||||
ProviderCreateEmbeddingProviderContext,
|
||||
ProviderDeferSyntheticProfileAuthContext,
|
||||
@@ -87,14 +87,7 @@ import type {
|
||||
const log = createSubsystemLogger("plugins/provider-runtime");
|
||||
const warnedExternalAuthFallbackPluginIds = new Set<string>();
|
||||
let catalogHookProvidersCache = new WeakMap<NodeJS.ProcessEnv, Map<string, ProviderPlugin[]>>();
|
||||
let catalogHookProviderIdCacheWithoutConfig = new WeakMap<
|
||||
NodeJS.ProcessEnv,
|
||||
Map<string, string[]>
|
||||
>();
|
||||
let catalogHookProviderIdCacheByConfig = new WeakMap<
|
||||
OpenClawConfig,
|
||||
WeakMap<NodeJS.ProcessEnv, Map<string, string[]>>
|
||||
>();
|
||||
let catalogHookProviderIdCache = new WeakMap<NodeJS.ProcessEnv, Map<string, string[]>>();
|
||||
|
||||
function matchesProviderPluginRef(provider: ProviderPlugin, providerId: string): boolean {
|
||||
const normalized = normalizeProviderId(providerId);
|
||||
@@ -155,35 +148,16 @@ function resetCatalogHookProvidersCacheForTest(): void {
|
||||
}
|
||||
|
||||
function clearCatalogHookProviderIdCache(): void {
|
||||
catalogHookProviderIdCacheWithoutConfig = new WeakMap<NodeJS.ProcessEnv, Map<string, string[]>>();
|
||||
catalogHookProviderIdCacheByConfig = new WeakMap<
|
||||
OpenClawConfig,
|
||||
WeakMap<NodeJS.ProcessEnv, Map<string, string[]>>
|
||||
>();
|
||||
catalogHookProviderIdCache = new WeakMap<NodeJS.ProcessEnv, Map<string, string[]>>();
|
||||
}
|
||||
|
||||
function resolveCatalogHookProviderIdCacheBucket(params: {
|
||||
config?: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): Map<string, string[]> {
|
||||
if (!params.config) {
|
||||
let bucket = catalogHookProviderIdCacheWithoutConfig.get(params.env);
|
||||
if (!bucket) {
|
||||
bucket = new Map<string, string[]>();
|
||||
catalogHookProviderIdCacheWithoutConfig.set(params.env, bucket);
|
||||
}
|
||||
return bucket;
|
||||
}
|
||||
|
||||
let envBuckets = catalogHookProviderIdCacheByConfig.get(params.config);
|
||||
if (!envBuckets) {
|
||||
envBuckets = new WeakMap<NodeJS.ProcessEnv, Map<string, string[]>>();
|
||||
catalogHookProviderIdCacheByConfig.set(params.config, envBuckets);
|
||||
}
|
||||
let bucket = envBuckets.get(params.env);
|
||||
let bucket = catalogHookProviderIdCache.get(params.env);
|
||||
if (!bucket) {
|
||||
bucket = new Map<string, string[]>();
|
||||
envBuckets.set(params.env, bucket);
|
||||
catalogHookProviderIdCache.set(params.env, bucket);
|
||||
}
|
||||
return bucket;
|
||||
}
|
||||
@@ -197,7 +171,7 @@ function buildCatalogHookProviderIdCacheKey(params: {
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
return `${roots.workspace ?? ""}::${roots.global}::${roots.stock ?? ""}::${JSON.stringify(params.config ?? null)}`;
|
||||
return `${roots.workspace ?? ""}::${roots.global}::${roots.stock ?? ""}::${JSON.stringify(resolveProviderHookConfigCacheShape(params.config, undefined))}`;
|
||||
}
|
||||
|
||||
function resolveCachedCatalogHookProviderPluginIds(params: {
|
||||
@@ -207,7 +181,6 @@ function resolveCachedCatalogHookProviderPluginIds(params: {
|
||||
}): string[] {
|
||||
const env = params.env ?? process.env;
|
||||
const bucket = resolveCatalogHookProviderIdCacheBucket({
|
||||
config: params.config,
|
||||
env,
|
||||
});
|
||||
const key = buildCatalogHookProviderIdCacheKey({
|
||||
@@ -266,19 +239,19 @@ function resolveProviderPluginsForCatalogHooks(params: {
|
||||
envCache = new Map<string, ProviderPlugin[]>();
|
||||
catalogHookProvidersCache.set(env, envCache);
|
||||
}
|
||||
const cacheKey = JSON.stringify({
|
||||
workspaceDir: workspaceDir ?? "",
|
||||
plugins: params.config?.plugins ?? null,
|
||||
});
|
||||
const cached = envCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const onlyPluginIds = resolveCachedCatalogHookProviderPluginIds({
|
||||
config: params.config,
|
||||
workspaceDir,
|
||||
env,
|
||||
});
|
||||
const cacheKey = JSON.stringify({
|
||||
workspaceDir: workspaceDir ?? "",
|
||||
plugins: resolveProviderHookConfigCacheShape(params.config, onlyPluginIds),
|
||||
});
|
||||
const cached = envCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
if (onlyPluginIds.length === 0) {
|
||||
envCache.set(cacheKey, []);
|
||||
return [];
|
||||
@@ -1096,24 +1069,6 @@ export function shouldDeferProviderSyntheticProfileAuthWithPlugin(params: {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveProviderBuiltInModelSuppression(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
context: ProviderBuiltInModelSuppressionContext;
|
||||
}) {
|
||||
// Deprecated compatibility fallback. Static suppression rules should live in
|
||||
// manifest modelCatalog.suppressions so list/model resolution can answer
|
||||
// without loading provider runtime.
|
||||
for (const plugin of resolveProviderPluginsForCatalogHooks(params)) {
|
||||
const result = plugin.suppressBuiltInModel?.(params.context);
|
||||
if (result?.suppress) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function augmentModelCatalogWithProviderPlugins(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
|
||||
@@ -1005,9 +1005,8 @@ export type ProviderBuildUnknownModelHintContext = {
|
||||
/**
|
||||
* Built-in model suppression hook context.
|
||||
*
|
||||
* @deprecated Use manifest `modelCatalog.suppressions` for static suppression
|
||||
* rules. Runtime suppression hooks remain as compatibility fallback for
|
||||
* plugins that cannot express a rule declaratively yet.
|
||||
* @deprecated Use manifest `modelCatalog.suppressions`. Runtime suppression
|
||||
* hooks are no longer called by model resolution.
|
||||
*/
|
||||
export type ProviderBuiltInModelSuppressionContext = {
|
||||
config?: OpenClawConfig;
|
||||
@@ -1518,9 +1517,8 @@ export type ProviderPlugin = {
|
||||
* `errorMessage` when OpenClaw should surface a provider-specific hint for
|
||||
* direct model resolution failures.
|
||||
*
|
||||
* @deprecated Use manifest `modelCatalog.suppressions` for static suppression
|
||||
* rules. Runtime suppression hooks remain as compatibility fallback for
|
||||
* plugins that cannot express a rule declaratively yet.
|
||||
* @deprecated Use manifest `modelCatalog.suppressions`. Runtime suppression
|
||||
* hooks are no longer called by model resolution.
|
||||
*/
|
||||
suppressBuiltInModel?: (
|
||||
ctx: ProviderBuiltInModelSuppressionContext,
|
||||
|
||||
Reference in New Issue
Block a user