mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-28 10:22:32 +00:00
refactor: derive bundled contracts from extension manifests
This commit is contained in:
@@ -46,6 +46,8 @@ Use it for:
|
||||
- config validation
|
||||
- auth and onboarding metadata that should be available without booting plugin
|
||||
runtime
|
||||
- static capability ownership snapshots used for bundled compat wiring and
|
||||
contract coverage
|
||||
- config UI hints
|
||||
|
||||
Do not use it for:
|
||||
@@ -129,6 +131,7 @@ Those belong in your plugin code and `package.json`.
|
||||
| `cliBackends` | No | `string[]` | CLI inference backend ids owned by this plugin. Used for startup auto-activation from explicit config refs. |
|
||||
| `providerAuthEnvVars` | No | `Record<string, string[]>` | Cheap provider-auth env metadata that OpenClaw can inspect without loading plugin code. |
|
||||
| `providerAuthChoices` | No | `object[]` | Cheap auth-choice metadata for onboarding pickers, preferred-provider resolution, and simple CLI flag wiring. |
|
||||
| `contracts` | No | `object` | Static bundled capability snapshot for speech, media-understanding, image-generation, web search, and tool ownership. |
|
||||
| `skills` | No | `string[]` | Skill directories to load, relative to the plugin root. |
|
||||
| `name` | No | `string` | Human-readable plugin name. |
|
||||
| `description` | No | `string` | Short summary shown in plugin surfaces. |
|
||||
@@ -184,6 +187,33 @@ Each field hint can include:
|
||||
| `sensitive` | `boolean` | Marks the field as secret or sensitive. |
|
||||
| `placeholder` | `string` | Placeholder text for form inputs. |
|
||||
|
||||
## contracts reference
|
||||
|
||||
Use `contracts` only for static capability ownership metadata that OpenClaw can
|
||||
read without importing the plugin runtime.
|
||||
|
||||
```json
|
||||
{
|
||||
"contracts": {
|
||||
"speechProviders": ["openai"],
|
||||
"mediaUnderstandingProviders": ["openai", "openai-codex"],
|
||||
"imageGenerationProviders": ["openai"],
|
||||
"webSearchProviders": ["gemini"],
|
||||
"tools": ["firecrawl_search", "firecrawl_scrape"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each list is optional:
|
||||
|
||||
| Field | Type | What it means |
|
||||
| ----------------------------- | ---------- | -------------------------------------------------------------- |
|
||||
| `speechProviders` | `string[]` | Speech provider ids this plugin owns. |
|
||||
| `mediaUnderstandingProviders` | `string[]` | Media-understanding provider ids this plugin owns. |
|
||||
| `imageGenerationProviders` | `string[]` | Image-generation provider ids this plugin owns. |
|
||||
| `webSearchProviders` | `string[]` | Web-search provider ids this plugin owns. |
|
||||
| `tools` | `string[]` | Agent tool names this plugin owns for bundled contract checks. |
|
||||
|
||||
## Manifest versus package.json
|
||||
|
||||
The two files serve different jobs:
|
||||
|
||||
@@ -41,6 +41,9 @@
|
||||
"cliDescription": "Anthropic API key"
|
||||
}
|
||||
],
|
||||
"contracts": {
|
||||
"mediaUnderstandingProviders": ["anthropic"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
"help": "Brave Search mode: web or llm-context."
|
||||
}
|
||||
},
|
||||
"contracts": {
|
||||
"webSearchProviders": ["brave"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "deepgram",
|
||||
"mediaUnderstandingProviders": ["deepgram"],
|
||||
"contracts": {
|
||||
"mediaUnderstandingProviders": ["deepgram"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"help": "SafeSearch level for DuckDuckGo results."
|
||||
}
|
||||
},
|
||||
"contracts": {
|
||||
"webSearchProviders": ["duckduckgo"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "elevenlabs",
|
||||
"speechProviders": ["elevenlabs"],
|
||||
"contracts": {
|
||||
"speechProviders": ["elevenlabs"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
"placeholder": "exa-..."
|
||||
}
|
||||
},
|
||||
"contracts": {
|
||||
"webSearchProviders": ["exa"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
"cliDescription": "fal API key"
|
||||
}
|
||||
],
|
||||
"contracts": {
|
||||
"imageGenerationProviders": ["fal"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
"help": "Firecrawl Search base URL override."
|
||||
}
|
||||
},
|
||||
"contracts": {
|
||||
"webSearchProviders": ["firecrawl"],
|
||||
"tools": ["firecrawl_search", "firecrawl_scrape"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -44,6 +44,11 @@
|
||||
"help": "Gemini model override for web search grounding."
|
||||
}
|
||||
},
|
||||
"contracts": {
|
||||
"mediaUnderstandingProviders": ["google"],
|
||||
"imageGenerationProviders": ["google"],
|
||||
"webSearchProviders": ["gemini"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "groq",
|
||||
"mediaUnderstandingProviders": ["groq"],
|
||||
"contracts": {
|
||||
"mediaUnderstandingProviders": ["groq"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "microsoft",
|
||||
"speechProviders": ["microsoft"],
|
||||
"contracts": {
|
||||
"speechProviders": ["microsoft"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -57,6 +57,10 @@
|
||||
"cliDescription": "MiniMax API key"
|
||||
}
|
||||
],
|
||||
"contracts": {
|
||||
"mediaUnderstandingProviders": ["minimax", "minimax-portal"],
|
||||
"imageGenerationProviders": ["minimax", "minimax-portal"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
"cliDescription": "Mistral API key"
|
||||
}
|
||||
],
|
||||
"contracts": {
|
||||
"mediaUnderstandingProviders": ["mistral"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -48,6 +48,10 @@
|
||||
"help": "Kimi model override."
|
||||
}
|
||||
},
|
||||
"contracts": {
|
||||
"mediaUnderstandingProviders": ["moonshot"],
|
||||
"webSearchProviders": ["kimi"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -33,6 +33,11 @@
|
||||
"cliDescription": "OpenAI API key"
|
||||
}
|
||||
],
|
||||
"contracts": {
|
||||
"speechProviders": ["openai"],
|
||||
"mediaUnderstandingProviders": ["openai", "openai-codex"],
|
||||
"imageGenerationProviders": ["openai"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
"help": "Optional Sonar/OpenRouter model override."
|
||||
}
|
||||
},
|
||||
"contracts": {
|
||||
"webSearchProviders": ["perplexity"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
"help": "Tavily API base URL override."
|
||||
}
|
||||
},
|
||||
"contracts": {
|
||||
"webSearchProviders": ["tavily"],
|
||||
"tools": ["tavily_search", "tavily_extract"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
"help": "Include inline markdown citations in Grok responses."
|
||||
}
|
||||
},
|
||||
"contracts": {
|
||||
"webSearchProviders": ["grok"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -76,6 +76,9 @@
|
||||
"cliDescription": "Z.AI API key"
|
||||
}
|
||||
],
|
||||
"contracts": {
|
||||
"mediaUnderstandingProviders": ["zai"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -50,6 +50,33 @@ function normalizeStringList(values) {
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function normalizeManifestContracts(raw) {
|
||||
const contracts = normalizeObject(raw);
|
||||
if (!contracts) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = {
|
||||
...(normalizeStringList(contracts.speechProviders)
|
||||
? { speechProviders: normalizeStringList(contracts.speechProviders) }
|
||||
: {}),
|
||||
...(normalizeStringList(contracts.mediaUnderstandingProviders)
|
||||
? {
|
||||
mediaUnderstandingProviders: normalizeStringList(contracts.mediaUnderstandingProviders),
|
||||
}
|
||||
: {}),
|
||||
...(normalizeStringList(contracts.imageGenerationProviders)
|
||||
? { imageGenerationProviders: normalizeStringList(contracts.imageGenerationProviders) }
|
||||
: {}),
|
||||
...(normalizeStringList(contracts.webSearchProviders)
|
||||
? { webSearchProviders: normalizeStringList(contracts.webSearchProviders) }
|
||||
: {}),
|
||||
...(normalizeStringList(contracts.tools)
|
||||
? { tools: normalizeStringList(contracts.tools) }
|
||||
: {}),
|
||||
};
|
||||
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function normalizeObject(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return undefined;
|
||||
@@ -109,6 +136,9 @@ function normalizePluginManifest(raw) {
|
||||
...(normalizeStringList(raw.imageGenerationProviders)
|
||||
? { imageGenerationProviders: normalizeStringList(raw.imageGenerationProviders) }
|
||||
: {}),
|
||||
...(normalizeStringList(raw.cliBackends)
|
||||
? { cliBackends: normalizeStringList(raw.cliBackends) }
|
||||
: {}),
|
||||
...(normalizeObject(raw.providerAuthEnvVars)
|
||||
? { providerAuthEnvVars: raw.providerAuthEnvVars }
|
||||
: {}),
|
||||
@@ -120,6 +150,9 @@ function normalizePluginManifest(raw) {
|
||||
...(typeof raw.description === "string" ? { description: raw.description.trim() } : {}),
|
||||
...(typeof raw.version === "string" ? { version: raw.version.trim() } : {}),
|
||||
...(normalizeObject(raw.uiHints) ? { uiHints: raw.uiHints } : {}),
|
||||
...(normalizeManifestContracts(raw.contracts)
|
||||
? { contracts: normalizeManifestContracts(raw.contracts) }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import bravePlugin from "../extensions/brave/index.js";
|
||||
import duckduckgoPlugin from "../extensions/duckduckgo/index.js";
|
||||
import exaPlugin from "../extensions/exa/index.js";
|
||||
import firecrawlPlugin from "../extensions/firecrawl/index.js";
|
||||
import googlePlugin from "../extensions/google/index.js";
|
||||
import moonshotPlugin from "../extensions/moonshot/index.js";
|
||||
import perplexityPlugin from "../extensions/perplexity/index.js";
|
||||
import tavilyPlugin from "../extensions/tavily/index.js";
|
||||
import xaiPlugin from "../extensions/xai/index.js";
|
||||
import type { OpenClawPluginApi } from "./plugins/types.js";
|
||||
|
||||
type RegistrablePlugin = {
|
||||
id: string;
|
||||
register: (api: OpenClawPluginApi) => void;
|
||||
};
|
||||
|
||||
export const bundledWebSearchPluginRegistrations: ReadonlyArray<{
|
||||
readonly plugin: RegistrablePlugin;
|
||||
credentialValue: unknown;
|
||||
}> = [
|
||||
{
|
||||
get plugin() {
|
||||
return bravePlugin;
|
||||
},
|
||||
credentialValue: "BSA-test",
|
||||
},
|
||||
{
|
||||
get plugin() {
|
||||
return exaPlugin;
|
||||
},
|
||||
credentialValue: "exa-test",
|
||||
},
|
||||
{
|
||||
get plugin() {
|
||||
return duckduckgoPlugin;
|
||||
},
|
||||
credentialValue: "duckduckgo-no-key-needed",
|
||||
},
|
||||
{
|
||||
get plugin() {
|
||||
return firecrawlPlugin;
|
||||
},
|
||||
credentialValue: "fc-test",
|
||||
},
|
||||
{
|
||||
get plugin() {
|
||||
return googlePlugin;
|
||||
},
|
||||
credentialValue: "AIza-test",
|
||||
},
|
||||
{
|
||||
get plugin() {
|
||||
return moonshotPlugin;
|
||||
},
|
||||
credentialValue: "sk-test",
|
||||
},
|
||||
{
|
||||
get plugin() {
|
||||
return perplexityPlugin;
|
||||
},
|
||||
credentialValue: "pplx-test",
|
||||
},
|
||||
{
|
||||
get plugin() {
|
||||
return tavilyPlugin;
|
||||
},
|
||||
credentialValue: "tvly-test",
|
||||
},
|
||||
{
|
||||
get plugin() {
|
||||
return xaiPlugin;
|
||||
},
|
||||
credentialValue: "xai-test",
|
||||
},
|
||||
];
|
||||
92
src/plugins/bundled-capability-metadata.ts
Normal file
92
src/plugins/bundled-capability-metadata.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { BUNDLED_PLUGIN_METADATA } from "./bundled-plugin-metadata.js";
|
||||
|
||||
export type BundledPluginContractSnapshot = {
|
||||
pluginId: string;
|
||||
cliBackendIds: string[];
|
||||
providerIds: string[];
|
||||
speechProviderIds: string[];
|
||||
mediaUnderstandingProviderIds: string[];
|
||||
imageGenerationProviderIds: string[];
|
||||
webSearchProviderIds: string[];
|
||||
toolNames: string[];
|
||||
};
|
||||
|
||||
function uniqueStrings(values: readonly string[] | undefined): string[] {
|
||||
const result: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const value of values ?? []) {
|
||||
const normalized = value.trim();
|
||||
if (!normalized || seen.has(normalized)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(normalized);
|
||||
result.push(normalized);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS: readonly BundledPluginContractSnapshot[] =
|
||||
BUNDLED_PLUGIN_METADATA.map(({ manifest }) => ({
|
||||
pluginId: manifest.id,
|
||||
cliBackendIds: uniqueStrings(manifest.cliBackends),
|
||||
providerIds: uniqueStrings(manifest.providers),
|
||||
speechProviderIds: uniqueStrings(manifest.contracts?.speechProviders),
|
||||
mediaUnderstandingProviderIds: uniqueStrings(manifest.contracts?.mediaUnderstandingProviders),
|
||||
imageGenerationProviderIds: uniqueStrings(manifest.contracts?.imageGenerationProviders),
|
||||
webSearchProviderIds: uniqueStrings(manifest.contracts?.webSearchProviders),
|
||||
toolNames: uniqueStrings(manifest.contracts?.tools),
|
||||
}))
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.cliBackendIds.length > 0 ||
|
||||
entry.providerIds.length > 0 ||
|
||||
entry.speechProviderIds.length > 0 ||
|
||||
entry.mediaUnderstandingProviderIds.length > 0 ||
|
||||
entry.imageGenerationProviderIds.length > 0 ||
|
||||
entry.webSearchProviderIds.length > 0 ||
|
||||
entry.toolNames.length > 0,
|
||||
)
|
||||
.toSorted((left, right) => left.pluginId.localeCompare(right.pluginId));
|
||||
|
||||
function collectPluginIds(
|
||||
pick: (entry: BundledPluginContractSnapshot) => readonly string[],
|
||||
): readonly string[] {
|
||||
return BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.filter((entry) => pick(entry).length > 0)
|
||||
.map((entry) => entry.pluginId)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export const BUNDLED_PROVIDER_PLUGIN_IDS = collectPluginIds((entry) => entry.providerIds);
|
||||
|
||||
export const BUNDLED_SPEECH_PLUGIN_IDS = collectPluginIds((entry) => entry.speechProviderIds);
|
||||
|
||||
export const BUNDLED_MEDIA_UNDERSTANDING_PLUGIN_IDS = collectPluginIds(
|
||||
(entry) => entry.mediaUnderstandingProviderIds,
|
||||
);
|
||||
|
||||
export const BUNDLED_IMAGE_GENERATION_PLUGIN_IDS = collectPluginIds(
|
||||
(entry) => entry.imageGenerationProviderIds,
|
||||
);
|
||||
|
||||
export const BUNDLED_RUNTIME_CONTRACT_PLUGIN_IDS = [
|
||||
...new Set(
|
||||
BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.filter(
|
||||
(entry) =>
|
||||
entry.providerIds.length > 0 ||
|
||||
entry.speechProviderIds.length > 0 ||
|
||||
entry.mediaUnderstandingProviderIds.length > 0 ||
|
||||
entry.imageGenerationProviderIds.length > 0 ||
|
||||
entry.webSearchProviderIds.length > 0,
|
||||
).map((entry) => entry.pluginId),
|
||||
),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
|
||||
export const BUNDLED_WEB_SEARCH_PLUGIN_IDS = collectPluginIds(
|
||||
(entry) => entry.webSearchProviderIds,
|
||||
);
|
||||
|
||||
export const BUNDLED_WEB_SEARCH_PROVIDER_PLUGIN_IDS = Object.fromEntries(
|
||||
BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.flatMap((entry) =>
|
||||
entry.webSearchProviderIds.map((providerId) => [providerId, entry.pluginId] as const),
|
||||
).toSorted(([left], [right]) => left.localeCompare(right)),
|
||||
) as Readonly<Record<string, string>>;
|
||||
41
src/plugins/bundled-capability-runtime.ts
Normal file
41
src/plugins/bundled-capability-runtime.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import {
|
||||
withBundledPluginEnablementCompat,
|
||||
withBundledPluginVitestCompat,
|
||||
} from "./bundled-compat.js";
|
||||
import { loadOpenClawPlugins, type PluginLoadOptions } from "./loader.js";
|
||||
|
||||
const log = createSubsystemLogger("plugins");
|
||||
|
||||
export function buildBundledCapabilityRuntimeConfig(
|
||||
pluginIds: readonly string[],
|
||||
env?: PluginLoadOptions["env"],
|
||||
): PluginLoadOptions["config"] {
|
||||
const enablementCompat = withBundledPluginEnablementCompat({
|
||||
config: undefined,
|
||||
pluginIds,
|
||||
});
|
||||
return withBundledPluginVitestCompat({
|
||||
config: enablementCompat,
|
||||
pluginIds,
|
||||
env,
|
||||
});
|
||||
}
|
||||
|
||||
export function loadBundledCapabilityRuntimeRegistry(params: {
|
||||
pluginIds: readonly string[];
|
||||
env?: PluginLoadOptions["env"];
|
||||
}) {
|
||||
return loadOpenClawPlugins({
|
||||
config: buildBundledCapabilityRuntimeConfig(params.pluginIds, params.env),
|
||||
env: params.env,
|
||||
onlyPluginIds: [...params.pluginIds],
|
||||
cache: false,
|
||||
activate: false,
|
||||
logger: {
|
||||
info: (message) => log.info(message),
|
||||
warn: (message) => log.warn(message),
|
||||
error: (message) => log.error(message),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -170,6 +170,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
},
|
||||
providers: ["anthropic"],
|
||||
mediaUnderstandingProviders: ["anthropic"],
|
||||
cliBackends: ["claude-cli"],
|
||||
providerAuthEnvVars: {
|
||||
anthropic: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
|
||||
},
|
||||
@@ -208,6 +209,9 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
cliDescription: "Anthropic API key",
|
||||
},
|
||||
],
|
||||
contracts: {
|
||||
mediaUnderstandingProviders: ["anthropic"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -306,6 +310,9 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
help: "Brave Search mode: web or llm-context.",
|
||||
},
|
||||
},
|
||||
contracts: {
|
||||
webSearchProviders: ["brave"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -523,6 +530,9 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
properties: {},
|
||||
},
|
||||
mediaUnderstandingProviders: ["deepgram"],
|
||||
contracts: {
|
||||
mediaUnderstandingProviders: ["deepgram"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -872,6 +882,9 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
help: "SafeSearch level for DuckDuckGo results.",
|
||||
},
|
||||
},
|
||||
contracts: {
|
||||
webSearchProviders: ["duckduckgo"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -895,6 +908,9 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
properties: {},
|
||||
},
|
||||
speechProviders: ["elevenlabs"],
|
||||
contracts: {
|
||||
speechProviders: ["elevenlabs"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -938,6 +954,9 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
placeholder: "exa-...",
|
||||
},
|
||||
},
|
||||
contracts: {
|
||||
webSearchProviders: ["exa"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -981,6 +1000,9 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
cliDescription: "fal API key",
|
||||
},
|
||||
],
|
||||
contracts: {
|
||||
imageGenerationProviders: ["fal"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1077,6 +1099,10 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
help: "Firecrawl Search base URL override.",
|
||||
},
|
||||
},
|
||||
contracts: {
|
||||
webSearchProviders: ["firecrawl"],
|
||||
tools: ["firecrawl_search", "firecrawl_scrape"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1153,6 +1179,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
providers: ["google", "google-gemini-cli"],
|
||||
mediaUnderstandingProviders: ["google"],
|
||||
imageGenerationProviders: ["google"],
|
||||
cliBackends: ["google-gemini-cli"],
|
||||
providerAuthEnvVars: {
|
||||
google: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
||||
},
|
||||
@@ -1193,6 +1220,11 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
help: "Gemini model override for web search grounding.",
|
||||
},
|
||||
},
|
||||
contracts: {
|
||||
mediaUnderstandingProviders: ["google"],
|
||||
imageGenerationProviders: ["google"],
|
||||
webSearchProviders: ["gemini"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1261,6 +1293,9 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
properties: {},
|
||||
},
|
||||
mediaUnderstandingProviders: ["groq"],
|
||||
contracts: {
|
||||
mediaUnderstandingProviders: ["groq"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1823,6 +1858,9 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
properties: {},
|
||||
},
|
||||
speechProviders: ["microsoft"],
|
||||
contracts: {
|
||||
speechProviders: ["microsoft"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1951,6 +1989,10 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
cliDescription: "MiniMax API key",
|
||||
},
|
||||
],
|
||||
contracts: {
|
||||
mediaUnderstandingProviders: ["minimax", "minimax-portal"],
|
||||
imageGenerationProviders: ["minimax", "minimax-portal"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1993,6 +2035,9 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
cliDescription: "Mistral API key",
|
||||
},
|
||||
],
|
||||
contracts: {
|
||||
mediaUnderstandingProviders: ["mistral"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -2163,6 +2208,10 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
help: "Kimi model override.",
|
||||
},
|
||||
},
|
||||
contracts: {
|
||||
mediaUnderstandingProviders: ["moonshot"],
|
||||
webSearchProviders: ["kimi"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -2411,6 +2460,7 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
speechProviders: ["openai"],
|
||||
mediaUnderstandingProviders: ["openai", "openai-codex"],
|
||||
imageGenerationProviders: ["openai"],
|
||||
cliBackends: ["codex-cli"],
|
||||
providerAuthEnvVars: {
|
||||
openai: ["OPENAI_API_KEY"],
|
||||
},
|
||||
@@ -2439,6 +2489,11 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
cliDescription: "OpenAI API key",
|
||||
},
|
||||
],
|
||||
contracts: {
|
||||
speechProviders: ["openai"],
|
||||
mediaUnderstandingProviders: ["openai", "openai-codex"],
|
||||
imageGenerationProviders: ["openai"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -2733,6 +2788,9 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
help: "Optional Sonar/OpenRouter model override.",
|
||||
},
|
||||
},
|
||||
contracts: {
|
||||
webSearchProviders: ["perplexity"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -3023,6 +3081,10 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
help: "Tavily API base URL override.",
|
||||
},
|
||||
},
|
||||
contracts: {
|
||||
webSearchProviders: ["tavily"],
|
||||
tools: ["tavily_search", "tavily_extract"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -4085,6 +4147,9 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
help: "Include inline markdown citations in Grok responses.",
|
||||
},
|
||||
},
|
||||
contracts: {
|
||||
webSearchProviders: ["grok"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -4224,6 +4289,9 @@ export const GENERATED_BUNDLED_PLUGIN_METADATA = [
|
||||
cliDescription: "Z.AI API key",
|
||||
},
|
||||
],
|
||||
contracts: {
|
||||
mediaUnderstandingProviders: ["zai"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
export const BUNDLED_WEB_SEARCH_PLUGIN_IDS = [
|
||||
"brave",
|
||||
"duckduckgo",
|
||||
"exa",
|
||||
"firecrawl",
|
||||
"google",
|
||||
"moonshot",
|
||||
"perplexity",
|
||||
"tavily",
|
||||
"xai",
|
||||
] as const;
|
||||
import { BUNDLED_WEB_SEARCH_PLUGIN_IDS as BUNDLED_WEB_SEARCH_PLUGIN_IDS_FROM_METADATA } from "./bundled-capability-metadata.js";
|
||||
|
||||
export const BUNDLED_WEB_SEARCH_PLUGIN_IDS = BUNDLED_WEB_SEARCH_PLUGIN_IDS_FROM_METADATA;
|
||||
|
||||
export function listBundledWebSearchPluginIds(): string[] {
|
||||
return [...BUNDLED_WEB_SEARCH_PLUGIN_IDS];
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
const BUNDLED_WEB_SEARCH_PROVIDER_PLUGIN_IDS = {
|
||||
brave: "brave",
|
||||
exa: "exa",
|
||||
firecrawl: "firecrawl",
|
||||
gemini: "google",
|
||||
grok: "xai",
|
||||
kimi: "moonshot",
|
||||
perplexity: "perplexity",
|
||||
tavily: "tavily",
|
||||
} as const satisfies Record<string, string>;
|
||||
import { BUNDLED_WEB_SEARCH_PROVIDER_PLUGIN_IDS } from "./bundled-capability-metadata.js";
|
||||
|
||||
export function resolveBundledWebSearchPluginId(
|
||||
providerId: string | undefined,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { bundledWebSearchPluginRegistrations } from "../bundled-web-search-registry.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { BUNDLED_WEB_SEARCH_PLUGIN_IDS } from "./bundled-web-search-ids.js";
|
||||
import { resolveBundledWebSearchPluginId } from "./bundled-web-search-provider-ids.js";
|
||||
@@ -85,14 +84,16 @@ describe("bundled web search metadata", () => {
|
||||
|
||||
it("keeps bundled web search fast-path ids aligned with the registry", () => {
|
||||
expect([...BUNDLED_WEB_SEARCH_PLUGIN_IDS]).toEqual(
|
||||
bundledWebSearchPluginRegistrations
|
||||
.map(({ plugin }) => plugin.id)
|
||||
listBundledWebSearchProviders()
|
||||
.map(({ pluginId }) => pluginId)
|
||||
.filter((value, index, values) => values.indexOf(value) === index)
|
||||
.toSorted((left, right) => left.localeCompare(right)),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps bundled web search provider-to-plugin ids aligned with bundled contracts", () => {
|
||||
expect(resolveBundledWebSearchPluginId("brave")).toBe("brave");
|
||||
expect(resolveBundledWebSearchPluginId("duckduckgo")).toBe("duckduckgo");
|
||||
expect(resolveBundledWebSearchPluginId("exa")).toBe("exa");
|
||||
expect(resolveBundledWebSearchPluginId("firecrawl")).toBe("firecrawl");
|
||||
expect(resolveBundledWebSearchPluginId("gemini")).toBe("google");
|
||||
@@ -102,7 +103,7 @@ describe("bundled web search metadata", () => {
|
||||
expect(resolveBundledWebSearchPluginId("grok")).toBe("xai");
|
||||
});
|
||||
|
||||
it("keeps fast-path bundled provider metadata aligned with bundled plugin contracts", async () => {
|
||||
it("keeps bundled provider metadata aligned with bundled plugin contracts", async () => {
|
||||
const fastPathProviders = listBundledWebSearchProviders();
|
||||
|
||||
expect(
|
||||
|
||||
@@ -1,50 +1,20 @@
|
||||
import { bundledWebSearchPluginRegistrations } from "../bundled-web-search-registry.js";
|
||||
import { listBundledWebSearchPluginIds as listBundledWebSearchPluginIdsFromIds } from "./bundled-web-search-ids.js";
|
||||
import { BUNDLED_WEB_SEARCH_PLUGIN_IDS } from "./bundled-capability-metadata.js";
|
||||
import { resolveBundledWebSearchPluginId as resolveBundledWebSearchPluginIdFromMap } from "./bundled-web-search-provider-ids.js";
|
||||
import { capturePluginRegistration } from "./captured-registration.js";
|
||||
import { webSearchProviderContractRegistry } from "./contracts/registry.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import type { PluginWebSearchProviderEntry } from "./types.js";
|
||||
|
||||
type BundledWebSearchProviderEntry = PluginWebSearchProviderEntry & { pluginId: string };
|
||||
type BundledWebSearchPluginRegistration = (typeof bundledWebSearchPluginRegistrations)[number];
|
||||
|
||||
let bundledWebSearchProvidersCache: BundledWebSearchProviderEntry[] | null = null;
|
||||
|
||||
function resolveBundledWebSearchPlugin(
|
||||
entry: BundledWebSearchPluginRegistration,
|
||||
): BundledWebSearchPluginRegistration["plugin"] | null {
|
||||
try {
|
||||
return entry.plugin;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function listBundledWebSearchPluginRegistrations() {
|
||||
return bundledWebSearchPluginRegistrations
|
||||
.map((entry) => {
|
||||
const plugin = resolveBundledWebSearchPlugin(entry);
|
||||
return plugin ? { ...entry, plugin } : null;
|
||||
})
|
||||
.filter(
|
||||
(
|
||||
entry,
|
||||
): entry is BundledWebSearchPluginRegistration & {
|
||||
plugin: BundledWebSearchPluginRegistration["plugin"];
|
||||
} => Boolean(entry),
|
||||
);
|
||||
}
|
||||
|
||||
function loadBundledWebSearchProviders(): BundledWebSearchProviderEntry[] {
|
||||
if (!bundledWebSearchProvidersCache) {
|
||||
bundledWebSearchProvidersCache = listBundledWebSearchPluginRegistrations().flatMap(
|
||||
({ plugin }) =>
|
||||
capturePluginRegistration(plugin).webSearchProviders.map((provider) => ({
|
||||
...provider,
|
||||
pluginId: plugin.id,
|
||||
})),
|
||||
);
|
||||
bundledWebSearchProvidersCache = webSearchProviderContractRegistry.map((entry) => ({
|
||||
...entry.provider,
|
||||
pluginId: entry.pluginId,
|
||||
}));
|
||||
}
|
||||
return bundledWebSearchProvidersCache;
|
||||
}
|
||||
@@ -54,20 +24,21 @@ export function resolveBundledWebSearchPluginIds(params: {
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
const registry = loadPluginManifestRegistry({
|
||||
const bundledWebSearchPluginIdSet = new Set<string>(BUNDLED_WEB_SEARCH_PLUGIN_IDS);
|
||||
return loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
const bundledWebSearchPluginIdSet = new Set<string>(listBundledWebSearchPluginIdsFromIds());
|
||||
return registry.plugins
|
||||
.filter((plugin) => plugin.origin === "bundled" && bundledWebSearchPluginIdSet.has(plugin.id))
|
||||
})
|
||||
.plugins.filter(
|
||||
(plugin) => plugin.origin === "bundled" && bundledWebSearchPluginIdSet.has(plugin.id),
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function listBundledWebSearchPluginIds(): string[] {
|
||||
return listBundledWebSearchPluginIdsFromIds();
|
||||
return [...BUNDLED_WEB_SEARCH_PLUGIN_IDS];
|
||||
}
|
||||
|
||||
export function listBundledWebSearchProviders(): PluginWebSearchProviderEntry[] {
|
||||
|
||||
@@ -7,23 +7,21 @@ import {
|
||||
pluginRegistrationContractRegistry,
|
||||
providerContractLoadError,
|
||||
providerContractPluginIds,
|
||||
providerContractRegistry,
|
||||
speechProviderContractRegistry,
|
||||
webSearchProviderContractRegistry,
|
||||
} from "./registry.js";
|
||||
|
||||
function findProviderIdsForPlugin(pluginId: string) {
|
||||
return providerContractRegistry
|
||||
.filter((entry) => entry.pluginId === pluginId)
|
||||
.map((entry) => entry.provider.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
return (
|
||||
pluginRegistrationContractRegistry.find((entry) => entry.pluginId === pluginId)?.providerIds ??
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
function findWebSearchIdsForPlugin(pluginId: string) {
|
||||
return webSearchProviderContractRegistry
|
||||
.filter((entry) => entry.pluginId === pluginId)
|
||||
.map((entry) => entry.provider.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
return (
|
||||
pluginRegistrationContractRegistry.find((entry) => entry.pluginId === pluginId)
|
||||
?.webSearchProviderIds ?? []
|
||||
);
|
||||
}
|
||||
|
||||
function findSpeechProviderIdsForPlugin(pluginId: string) {
|
||||
@@ -85,6 +83,20 @@ function findRegistrationForPlugin(pluginId: string) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
type BundledCapabilityContractKey =
|
||||
| "speechProviders"
|
||||
| "mediaUnderstandingProviders"
|
||||
| "imageGenerationProviders";
|
||||
|
||||
function findBundledManifestPluginIdsForContract(key: BundledCapabilityContractKey) {
|
||||
return loadPluginManifestRegistry({})
|
||||
.plugins.filter(
|
||||
(plugin) => plugin.origin === "bundled" && (plugin.contracts?.[key]?.length ?? 0) > 0,
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
describe("plugin contract registry", () => {
|
||||
it("loads bundled non-provider capability registries without import-time failure", () => {
|
||||
expect(providerContractLoadError).toBeUndefined();
|
||||
@@ -92,12 +104,12 @@ describe("plugin contract registry", () => {
|
||||
});
|
||||
|
||||
it("does not duplicate bundled provider ids", () => {
|
||||
const ids = providerContractRegistry.map((entry) => entry.provider.id);
|
||||
const ids = pluginRegistrationContractRegistry.flatMap((entry) => entry.providerIds);
|
||||
expect(ids).toEqual([...new Set(ids)]);
|
||||
});
|
||||
|
||||
it("does not duplicate bundled web search provider ids", () => {
|
||||
const ids = webSearchProviderContractRegistry.map((entry) => entry.provider.id);
|
||||
const ids = pluginRegistrationContractRegistry.flatMap((entry) => entry.webSearchProviderIds);
|
||||
expect(ids).toEqual([...new Set(ids)]);
|
||||
});
|
||||
|
||||
@@ -121,12 +133,7 @@ describe("plugin contract registry", () => {
|
||||
});
|
||||
|
||||
it("covers every bundled speech plugin discovered from manifests", () => {
|
||||
const bundledSpeechPluginIds = loadPluginManifestRegistry({})
|
||||
.plugins.filter(
|
||||
(plugin) => plugin.origin === "bundled" && (plugin.speechProviders?.length ?? 0) > 0,
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
const bundledSpeechPluginIds = findBundledManifestPluginIdsForContract("speechProviders");
|
||||
|
||||
expect(
|
||||
[...new Set(speechProviderContractRegistry.map((entry) => entry.pluginId))].toSorted(
|
||||
@@ -136,13 +143,9 @@ describe("plugin contract registry", () => {
|
||||
});
|
||||
|
||||
it("covers every bundled media-understanding plugin discovered from manifests", () => {
|
||||
const bundledMediaPluginIds = loadPluginManifestRegistry({})
|
||||
.plugins.filter(
|
||||
(plugin) =>
|
||||
plugin.origin === "bundled" && (plugin.mediaUnderstandingProviders?.length ?? 0) > 0,
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
const bundledMediaPluginIds = findBundledManifestPluginIdsForContract(
|
||||
"mediaUnderstandingProviders",
|
||||
);
|
||||
|
||||
expect(
|
||||
[
|
||||
@@ -152,13 +155,9 @@ describe("plugin contract registry", () => {
|
||||
});
|
||||
|
||||
it("covers every bundled image-generation plugin discovered from manifests", () => {
|
||||
const bundledImagePluginIds = loadPluginManifestRegistry({})
|
||||
.plugins.filter(
|
||||
(plugin) =>
|
||||
plugin.origin === "bundled" && (plugin.imageGenerationProviders?.length ?? 0) > 0,
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
const bundledImagePluginIds = findBundledManifestPluginIdsForContract(
|
||||
"imageGenerationProviders",
|
||||
);
|
||||
|
||||
expect(
|
||||
[...new Set(imageGenerationProviderContractRegistry.map((entry) => entry.pluginId))].toSorted(
|
||||
@@ -167,13 +166,28 @@ describe("plugin contract registry", () => {
|
||||
).toEqual(bundledImagePluginIds);
|
||||
});
|
||||
|
||||
it("keeps bundled legacy capability fields aligned with manifest contracts", () => {
|
||||
for (const plugin of loadPluginManifestRegistry({}).plugins.filter(
|
||||
(candidate) => candidate.origin === "bundled",
|
||||
)) {
|
||||
expect(plugin.speechProviders).toEqual(plugin.contracts?.speechProviders ?? []);
|
||||
expect(plugin.mediaUnderstandingProviders).toEqual(
|
||||
plugin.contracts?.mediaUnderstandingProviders ?? [],
|
||||
);
|
||||
expect(plugin.imageGenerationProviders).toEqual(
|
||||
plugin.contracts?.imageGenerationProviders ?? [],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("covers every bundled web search plugin from the shared resolver", () => {
|
||||
const bundledWebSearchPluginIds = resolveBundledWebSearchPluginIds({});
|
||||
|
||||
expect(
|
||||
[...new Set(webSearchProviderContractRegistry.map((entry) => entry.pluginId))].toSorted(
|
||||
(left, right) => left.localeCompare(right),
|
||||
),
|
||||
pluginRegistrationContractRegistry
|
||||
.filter((entry) => entry.webSearchProviderIds.length > 0)
|
||||
.map((entry) => entry.pluginId)
|
||||
.toSorted((left, right) => left.localeCompare(right)),
|
||||
).toEqual(bundledWebSearchPluginIds);
|
||||
});
|
||||
|
||||
@@ -207,6 +221,7 @@ describe("plugin contract registry", () => {
|
||||
|
||||
it("keeps bundled web search ownership explicit", () => {
|
||||
expect(findWebSearchIdsForPlugin("brave")).toEqual(["brave"]);
|
||||
expect(findWebSearchIdsForPlugin("duckduckgo")).toEqual(["duckduckgo"]);
|
||||
expect(findWebSearchIdsForPlugin("exa")).toEqual(["exa"]);
|
||||
expect(findWebSearchIdsForPlugin("firecrawl")).toEqual(["firecrawl"]);
|
||||
expect(findWebSearchIdsForPlugin("google")).toEqual(["gemini"]);
|
||||
@@ -241,6 +256,10 @@ describe("plugin contract registry", () => {
|
||||
it("keeps bundled image-generation ownership explicit", () => {
|
||||
expect(findImageGenerationProviderIdsForPlugin("fal")).toEqual(["fal"]);
|
||||
expect(findImageGenerationProviderIdsForPlugin("google")).toEqual(["google"]);
|
||||
expect(findImageGenerationProviderIdsForPlugin("minimax")).toEqual([
|
||||
"minimax",
|
||||
"minimax-portal",
|
||||
]);
|
||||
expect(findImageGenerationProviderIdsForPlugin("openai")).toEqual(["openai"]);
|
||||
});
|
||||
|
||||
@@ -306,6 +325,14 @@ describe("plugin contract registry", () => {
|
||||
mediaUnderstandingProviderIds: ["openai", "openai-codex"],
|
||||
imageGenerationProviderIds: ["openai"],
|
||||
});
|
||||
expect(findRegistrationForPlugin("minimax")).toMatchObject({
|
||||
cliBackendIds: [],
|
||||
providerIds: ["minimax", "minimax-portal"],
|
||||
speechProviderIds: [],
|
||||
mediaUnderstandingProviderIds: ["minimax", "minimax-portal"],
|
||||
imageGenerationProviderIds: ["minimax", "minimax-portal"],
|
||||
webSearchProviderIds: [],
|
||||
});
|
||||
expect(findRegistrationForPlugin("elevenlabs")).toMatchObject({
|
||||
cliBackendIds: [],
|
||||
providerIds: [],
|
||||
@@ -325,11 +352,15 @@ describe("plugin contract registry", () => {
|
||||
it("tracks every provider, speech, media, image, or web search plugin in the registration registry", () => {
|
||||
const expectedPluginIds = [
|
||||
...new Set([
|
||||
...providerContractRegistry.map((entry) => entry.pluginId),
|
||||
...pluginRegistrationContractRegistry
|
||||
.filter((entry) => entry.providerIds.length > 0)
|
||||
.map((entry) => entry.pluginId),
|
||||
...speechProviderContractRegistry.map((entry) => entry.pluginId),
|
||||
...mediaUnderstandingProviderContractRegistry.map((entry) => entry.pluginId),
|
||||
...imageGenerationProviderContractRegistry.map((entry) => entry.pluginId),
|
||||
...webSearchProviderContractRegistry.map((entry) => entry.pluginId),
|
||||
...pluginRegistrationContractRegistry
|
||||
.filter((entry) => entry.webSearchProviderIds.length > 0)
|
||||
.map((entry) => entry.pluginId),
|
||||
]),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
|
||||
@@ -371,6 +402,9 @@ describe("plugin contract registry", () => {
|
||||
expect(findImageGenerationProviderForPlugin("google").generateImage).toEqual(
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(findImageGenerationProviderForPlugin("minimax").generateImage).toEqual(
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(findImageGenerationProviderForPlugin("openai").generateImage).toEqual(
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { bundledWebSearchPluginRegistrations } from "../../bundled-web-search-registry.js";
|
||||
import { BUNDLED_PLUGIN_ENTRIES } from "../bundled-plugin-entries.js";
|
||||
import { createCapturedPluginRegistration } from "../captured-registration.js";
|
||||
import { loadPluginManifestRegistry } from "../manifest-registry.js";
|
||||
import {
|
||||
BUNDLED_IMAGE_GENERATION_PLUGIN_IDS,
|
||||
BUNDLED_MEDIA_UNDERSTANDING_PLUGIN_IDS,
|
||||
BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS,
|
||||
BUNDLED_PROVIDER_PLUGIN_IDS,
|
||||
BUNDLED_SPEECH_PLUGIN_IDS,
|
||||
BUNDLED_WEB_SEARCH_PLUGIN_IDS,
|
||||
} from "../bundled-capability-metadata.js";
|
||||
import { loadBundledCapabilityRuntimeRegistry } from "../bundled-capability-runtime.js";
|
||||
import { resolvePluginProviders } from "../providers.runtime.js";
|
||||
import type {
|
||||
ImageGenerationProviderPlugin,
|
||||
MediaUnderstandingProviderPlugin,
|
||||
@@ -10,11 +16,6 @@ import type {
|
||||
WebSearchProviderPlugin,
|
||||
} from "../types.js";
|
||||
|
||||
type RegistrablePlugin = {
|
||||
id: string;
|
||||
register: (api: ReturnType<typeof createCapturedPluginRegistration>["api"]) => void;
|
||||
};
|
||||
|
||||
type CapabilityContractEntry<T> = {
|
||||
pluginId: string;
|
||||
provider: T;
|
||||
@@ -42,42 +43,140 @@ type PluginRegistrationContractEntry = {
|
||||
toolNames: string[];
|
||||
};
|
||||
|
||||
const bundledWebSearchPlugins: Array<RegistrablePlugin & { credentialValue: unknown }> =
|
||||
bundledWebSearchPluginRegistrations.map(({ plugin, credentialValue }) => ({
|
||||
...plugin,
|
||||
credentialValue,
|
||||
}));
|
||||
|
||||
function captureRegistrations(plugin: RegistrablePlugin) {
|
||||
const captured = createCapturedPluginRegistration();
|
||||
plugin.register(captured.api);
|
||||
return captured;
|
||||
function uniqueStrings(values: readonly string[]): string[] {
|
||||
const result: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const value of values) {
|
||||
if (seen.has(value)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(value);
|
||||
result.push(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildCapabilityContractRegistry<T>(params: {
|
||||
plugins: RegistrablePlugin[];
|
||||
select: (captured: ReturnType<typeof createCapturedPluginRegistration>) => T[];
|
||||
}): CapabilityContractEntry<T>[] {
|
||||
return params.plugins.flatMap((plugin) => {
|
||||
const captured = captureRegistrations(plugin);
|
||||
return params.select(captured).map((provider) => ({
|
||||
pluginId: plugin.id,
|
||||
provider,
|
||||
}));
|
||||
});
|
||||
let providerContractRegistryCache: ProviderContractEntry[] | null = null;
|
||||
let webSearchProviderContractRegistryCache: WebSearchProviderContractEntry[] | null = null;
|
||||
let speechProviderContractRegistryCache: SpeechProviderContractEntry[] | null = null;
|
||||
let mediaUnderstandingProviderContractRegistryCache:
|
||||
| MediaUnderstandingProviderContractEntry[]
|
||||
| null = null;
|
||||
let imageGenerationProviderContractRegistryCache: ImageGenerationProviderContractEntry[] | null =
|
||||
null;
|
||||
|
||||
export let providerContractLoadError: Error | undefined;
|
||||
|
||||
function loadProviderContractRegistry(): ProviderContractEntry[] {
|
||||
if (!providerContractRegistryCache) {
|
||||
try {
|
||||
providerContractLoadError = undefined;
|
||||
providerContractRegistryCache = resolvePluginProviders({
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
onlyPluginIds: [...BUNDLED_PROVIDER_PLUGIN_IDS],
|
||||
cache: false,
|
||||
activate: false,
|
||||
}).map((provider) => ({
|
||||
pluginId: provider.pluginId ?? "",
|
||||
provider,
|
||||
}));
|
||||
} catch (error) {
|
||||
providerContractLoadError = error instanceof Error ? error : new Error(String(error));
|
||||
providerContractRegistryCache = [];
|
||||
}
|
||||
}
|
||||
return providerContractRegistryCache;
|
||||
}
|
||||
|
||||
function dedupePlugins<T extends RegistrablePlugin>(
|
||||
plugins: ReadonlyArray<T | undefined | null>,
|
||||
): T[] {
|
||||
function loadUniqueProviderContractProviders(): ProviderPlugin[] {
|
||||
return [
|
||||
...new Map(
|
||||
plugins.filter((plugin): plugin is T => Boolean(plugin)).map((plugin) => [plugin.id, plugin]),
|
||||
loadProviderContractRegistry().map((entry) => [entry.provider.id, entry.provider]),
|
||||
).values(),
|
||||
];
|
||||
}
|
||||
|
||||
export let providerContractLoadError: Error | undefined;
|
||||
function loadProviderContractPluginIds(): string[] {
|
||||
return [...BUNDLED_PROVIDER_PLUGIN_IDS];
|
||||
}
|
||||
|
||||
function loadProviderContractCompatPluginIds(): string[] {
|
||||
return loadProviderContractPluginIds().map((pluginId) =>
|
||||
pluginId === "kimi-coding" ? "kimi" : pluginId,
|
||||
);
|
||||
}
|
||||
|
||||
function resolveWebSearchCredentialValue(provider: WebSearchProviderPlugin): unknown {
|
||||
if (provider.requiresCredential === false) {
|
||||
return `${provider.id}-no-key-needed`;
|
||||
}
|
||||
const envVar = provider.envVars.find((entry) => entry.trim().length > 0);
|
||||
if (!envVar) {
|
||||
return `${provider.id}-test`;
|
||||
}
|
||||
if (envVar === "OPENROUTER_API_KEY") {
|
||||
return "openrouter-test";
|
||||
}
|
||||
return envVar.toLowerCase().includes("api_key") ? `${provider.id}-test` : "sk-test";
|
||||
}
|
||||
|
||||
function loadWebSearchProviderContractRegistry(): WebSearchProviderContractEntry[] {
|
||||
if (!webSearchProviderContractRegistryCache) {
|
||||
const registry = loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds: BUNDLED_WEB_SEARCH_PLUGIN_IDS,
|
||||
});
|
||||
webSearchProviderContractRegistryCache = registry.webSearchProviders.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
credentialValue: resolveWebSearchCredentialValue(entry.provider),
|
||||
}));
|
||||
}
|
||||
return webSearchProviderContractRegistryCache;
|
||||
}
|
||||
|
||||
function loadSpeechProviderContractRegistry(): SpeechProviderContractEntry[] {
|
||||
if (!speechProviderContractRegistryCache) {
|
||||
const registry = loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds: BUNDLED_SPEECH_PLUGIN_IDS,
|
||||
});
|
||||
speechProviderContractRegistryCache = registry.speechProviders.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
}));
|
||||
}
|
||||
return speechProviderContractRegistryCache;
|
||||
}
|
||||
|
||||
function loadMediaUnderstandingProviderContractRegistry(): MediaUnderstandingProviderContractEntry[] {
|
||||
if (!mediaUnderstandingProviderContractRegistryCache) {
|
||||
const registry = loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds: BUNDLED_MEDIA_UNDERSTANDING_PLUGIN_IDS,
|
||||
});
|
||||
mediaUnderstandingProviderContractRegistryCache = registry.mediaUnderstandingProviders.map(
|
||||
(entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return mediaUnderstandingProviderContractRegistryCache;
|
||||
}
|
||||
|
||||
function loadImageGenerationProviderContractRegistry(): ImageGenerationProviderContractEntry[] {
|
||||
if (!imageGenerationProviderContractRegistryCache) {
|
||||
const registry = loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds: BUNDLED_IMAGE_GENERATION_PLUGIN_IDS,
|
||||
});
|
||||
imageGenerationProviderContractRegistryCache = registry.imageGenerationProviders.map(
|
||||
(entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return imageGenerationProviderContractRegistryCache;
|
||||
}
|
||||
|
||||
function createLazyArrayView<T>(load: () => T[]): T[] {
|
||||
return new Proxy([] as T[], {
|
||||
@@ -111,55 +210,6 @@ function createLazyArrayView<T>(load: () => T[]): T[] {
|
||||
});
|
||||
}
|
||||
|
||||
let providerContractRegistryCache: ProviderContractEntry[] | null = null;
|
||||
let webSearchProviderContractRegistryCache: WebSearchProviderContractEntry[] | null = null;
|
||||
let speechProviderContractRegistryCache: SpeechProviderContractEntry[] | null = null;
|
||||
let mediaUnderstandingProviderContractRegistryCache:
|
||||
| MediaUnderstandingProviderContractEntry[]
|
||||
| null = null;
|
||||
let imageGenerationProviderContractRegistryCache: ImageGenerationProviderContractEntry[] | null =
|
||||
null;
|
||||
let pluginRegistrationContractRegistryCache: PluginRegistrationContractEntry[] | null = null;
|
||||
|
||||
function loadProviderContractRegistry(): ProviderContractEntry[] {
|
||||
if (!providerContractRegistryCache) {
|
||||
try {
|
||||
providerContractLoadError = undefined;
|
||||
providerContractRegistryCache = buildCapabilityContractRegistry({
|
||||
plugins: bundledProviderPlugins,
|
||||
select: (captured) => captured.providers,
|
||||
}).map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
provider: entry.provider,
|
||||
}));
|
||||
} catch (error) {
|
||||
providerContractLoadError = error instanceof Error ? error : new Error(String(error));
|
||||
providerContractRegistryCache = [];
|
||||
}
|
||||
}
|
||||
return providerContractRegistryCache;
|
||||
}
|
||||
|
||||
function loadUniqueProviderContractProviders(): ProviderPlugin[] {
|
||||
return [
|
||||
...new Map(
|
||||
loadProviderContractRegistry().map((entry) => [entry.provider.id, entry.provider]),
|
||||
).values(),
|
||||
];
|
||||
}
|
||||
|
||||
function loadProviderContractPluginIds(): string[] {
|
||||
return [...new Set(loadProviderContractRegistry().map((entry) => entry.pluginId))].toSorted(
|
||||
(left, right) => left.localeCompare(right),
|
||||
);
|
||||
}
|
||||
|
||||
function loadProviderContractCompatPluginIds(): string[] {
|
||||
return loadProviderContractPluginIds().map((pluginId) =>
|
||||
pluginId === "kimi-coding" ? "kimi" : pluginId,
|
||||
);
|
||||
}
|
||||
|
||||
export const providerContractRegistry: ProviderContractEntry[] = createLazyArrayView(
|
||||
loadProviderContractRegistry,
|
||||
);
|
||||
@@ -179,9 +229,6 @@ export const providerContractCompatPluginIds: string[] = createLazyArrayView(
|
||||
export function requireProviderContractProvider(providerId: string): ProviderPlugin {
|
||||
const provider = uniqueProviderContractProviders.find((entry) => entry.id === providerId);
|
||||
if (!provider) {
|
||||
if (!providerContractLoadError) {
|
||||
loadProviderContractRegistry();
|
||||
}
|
||||
if (providerContractLoadError) {
|
||||
throw new Error(
|
||||
`provider contract entry missing for ${providerId}; bundled provider registry failed to load: ${providerContractLoadError.message}`,
|
||||
@@ -218,50 +265,6 @@ export function resolveProviderContractProvidersForPluginIds(
|
||||
];
|
||||
}
|
||||
|
||||
function loadWebSearchProviderContractRegistry(): WebSearchProviderContractEntry[] {
|
||||
if (!webSearchProviderContractRegistryCache) {
|
||||
webSearchProviderContractRegistryCache = bundledWebSearchPlugins.flatMap((plugin) => {
|
||||
const captured = captureRegistrations(plugin);
|
||||
return captured.webSearchProviders.map((provider) => ({
|
||||
pluginId: plugin.id,
|
||||
provider,
|
||||
credentialValue: plugin.credentialValue,
|
||||
}));
|
||||
});
|
||||
}
|
||||
return webSearchProviderContractRegistryCache;
|
||||
}
|
||||
|
||||
function loadSpeechProviderContractRegistry(): SpeechProviderContractEntry[] {
|
||||
if (!speechProviderContractRegistryCache) {
|
||||
speechProviderContractRegistryCache = buildCapabilityContractRegistry({
|
||||
plugins: bundledSpeechPlugins,
|
||||
select: (captured) => captured.speechProviders,
|
||||
});
|
||||
}
|
||||
return speechProviderContractRegistryCache;
|
||||
}
|
||||
|
||||
function loadMediaUnderstandingProviderContractRegistry(): MediaUnderstandingProviderContractEntry[] {
|
||||
if (!mediaUnderstandingProviderContractRegistryCache) {
|
||||
mediaUnderstandingProviderContractRegistryCache = buildCapabilityContractRegistry({
|
||||
plugins: bundledMediaUnderstandingPlugins,
|
||||
select: (captured) => captured.mediaUnderstandingProviders,
|
||||
});
|
||||
}
|
||||
return mediaUnderstandingProviderContractRegistryCache;
|
||||
}
|
||||
|
||||
function loadImageGenerationProviderContractRegistry(): ImageGenerationProviderContractEntry[] {
|
||||
if (!imageGenerationProviderContractRegistryCache) {
|
||||
imageGenerationProviderContractRegistryCache = buildCapabilityContractRegistry({
|
||||
plugins: bundledImageGenerationPlugins,
|
||||
select: (captured) => captured.imageGenerationProviders,
|
||||
});
|
||||
}
|
||||
return imageGenerationProviderContractRegistryCache;
|
||||
}
|
||||
|
||||
export const webSearchProviderContractRegistry: WebSearchProviderContractEntry[] =
|
||||
createLazyArrayView(loadWebSearchProviderContractRegistry);
|
||||
|
||||
@@ -275,107 +278,17 @@ export const mediaUnderstandingProviderContractRegistry: MediaUnderstandingProvi
|
||||
export const imageGenerationProviderContractRegistry: ImageGenerationProviderContractEntry[] =
|
||||
createLazyArrayView(loadImageGenerationProviderContractRegistry);
|
||||
|
||||
const bundledRegistrablePluginsById = new Map(
|
||||
dedupePlugins([...BUNDLED_PLUGIN_ENTRIES, ...bundledWebSearchPlugins]).map((plugin) => [
|
||||
plugin.id,
|
||||
plugin,
|
||||
]),
|
||||
);
|
||||
|
||||
function resolveBundledManifestPluginIds(
|
||||
predicate: (plugin: ReturnType<typeof loadPluginManifestRegistry>["plugins"][number]) => boolean,
|
||||
): string[] {
|
||||
return loadPluginManifestRegistry({})
|
||||
.plugins.filter((plugin) => plugin.origin === "bundled" && predicate(plugin))
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function resolveBundledRegistrablePlugins(
|
||||
predicate: (plugin: ReturnType<typeof loadPluginManifestRegistry>["plugins"][number]) => boolean,
|
||||
): RegistrablePlugin[] {
|
||||
return resolveBundledManifestPluginIds(predicate).flatMap((pluginId) => {
|
||||
const plugin = bundledRegistrablePluginsById.get(pluginId);
|
||||
return plugin ? [plugin] : [];
|
||||
});
|
||||
}
|
||||
|
||||
const bundledProviderPlugins = resolveBundledRegistrablePlugins(
|
||||
(plugin) => plugin.providers.length > 0,
|
||||
);
|
||||
const bundledSpeechPlugins = resolveBundledRegistrablePlugins(
|
||||
(plugin) => (plugin.speechProviders?.length ?? 0) > 0,
|
||||
);
|
||||
const bundledMediaUnderstandingPlugins = resolveBundledRegistrablePlugins(
|
||||
(plugin) => (plugin.mediaUnderstandingProviders?.length ?? 0) > 0,
|
||||
);
|
||||
const bundledImageGenerationPlugins = resolveBundledRegistrablePlugins(
|
||||
(plugin) => (plugin.imageGenerationProviders?.length ?? 0) > 0,
|
||||
);
|
||||
|
||||
const bundledPluginRegistrationList = dedupePlugins([
|
||||
...bundledProviderPlugins,
|
||||
...bundledSpeechPlugins,
|
||||
...bundledMediaUnderstandingPlugins,
|
||||
...bundledImageGenerationPlugins,
|
||||
...bundledWebSearchPlugins,
|
||||
]);
|
||||
|
||||
function upsertPluginRegistrationContractEntry(
|
||||
entries: PluginRegistrationContractEntry[],
|
||||
next: PluginRegistrationContractEntry,
|
||||
): void {
|
||||
const existing = entries.find((entry) => entry.pluginId === next.pluginId);
|
||||
if (!existing) {
|
||||
entries.push(next);
|
||||
return;
|
||||
}
|
||||
existing.cliBackendIds = [
|
||||
...new Set([...existing.cliBackendIds, ...next.cliBackendIds]),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
existing.providerIds = [...new Set([...existing.providerIds, ...next.providerIds])].toSorted(
|
||||
(left, right) => left.localeCompare(right),
|
||||
);
|
||||
existing.speechProviderIds = [
|
||||
...new Set([...existing.speechProviderIds, ...next.speechProviderIds]),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
existing.mediaUnderstandingProviderIds = [
|
||||
...new Set([...existing.mediaUnderstandingProviderIds, ...next.mediaUnderstandingProviderIds]),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
existing.imageGenerationProviderIds = [
|
||||
...new Set([...existing.imageGenerationProviderIds, ...next.imageGenerationProviderIds]),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
existing.webSearchProviderIds = [
|
||||
...new Set([...existing.webSearchProviderIds, ...next.webSearchProviderIds]),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
existing.toolNames = [...new Set([...existing.toolNames, ...next.toolNames])].toSorted(
|
||||
(left, right) => left.localeCompare(right),
|
||||
);
|
||||
}
|
||||
|
||||
function loadPluginRegistrationContractRegistry(): PluginRegistrationContractEntry[] {
|
||||
if (!pluginRegistrationContractRegistryCache) {
|
||||
const entries: PluginRegistrationContractEntry[] = [];
|
||||
for (const plugin of bundledPluginRegistrationList) {
|
||||
const captured = captureRegistrations(plugin);
|
||||
upsertPluginRegistrationContractEntry(entries, {
|
||||
pluginId: plugin.id,
|
||||
cliBackendIds: captured.cliBackends.map((backend) => backend.id),
|
||||
providerIds: captured.providers.map((provider) => provider.id),
|
||||
speechProviderIds: captured.speechProviders.map((provider) => provider.id),
|
||||
mediaUnderstandingProviderIds: captured.mediaUnderstandingProviders.map(
|
||||
(provider) => provider.id,
|
||||
),
|
||||
imageGenerationProviderIds: captured.imageGenerationProviders.map(
|
||||
(provider) => provider.id,
|
||||
),
|
||||
webSearchProviderIds: captured.webSearchProviders.map((provider) => provider.id),
|
||||
toolNames: captured.tools.map((tool) => tool.name),
|
||||
});
|
||||
}
|
||||
pluginRegistrationContractRegistryCache = entries;
|
||||
}
|
||||
return pluginRegistrationContractRegistryCache;
|
||||
return BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
cliBackendIds: uniqueStrings(entry.cliBackendIds),
|
||||
providerIds: uniqueStrings(entry.providerIds),
|
||||
speechProviderIds: uniqueStrings(entry.speechProviderIds),
|
||||
mediaUnderstandingProviderIds: uniqueStrings(entry.mediaUnderstandingProviderIds),
|
||||
imageGenerationProviderIds: uniqueStrings(entry.imageGenerationProviderIds),
|
||||
webSearchProviderIds: uniqueStrings(entry.webSearchProviderIds),
|
||||
toolNames: uniqueStrings(entry.toolNames),
|
||||
}));
|
||||
}
|
||||
|
||||
export const pluginRegistrationContractRegistry: PluginRegistrationContractEntry[] =
|
||||
|
||||
@@ -2,11 +2,9 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import openAIPlugin from "../../../extensions/openai/index.js";
|
||||
import { createProviderUsageFetch, makeResponse } from "../../test-utils/provider-usage-fetch.js";
|
||||
import type { ProviderPlugin, ProviderRuntimeModel } from "../types.js";
|
||||
import { requireProviderContractProvider as requireBundledProviderContractProvider } from "./registry.js";
|
||||
import { registerProviders, requireProvider } from "./testkit.js";
|
||||
|
||||
const CONTRACT_SETUP_TIMEOUT_MS = 300_000;
|
||||
|
||||
@@ -46,9 +44,6 @@ function createModel(overrides: Partial<ProviderRuntimeModel> & Pick<ProviderRun
|
||||
}
|
||||
|
||||
function requireProviderContractProvider(providerId: string): ProviderPlugin {
|
||||
if (providerId === "openai-codex") {
|
||||
return requireProvider(registerProviders(openAIPlugin), providerId);
|
||||
}
|
||||
return requireBundledProviderContractProvider(providerId);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ import { resolveRuntimeServiceVersion } from "../version.js";
|
||||
import { loadBundleManifest } from "./bundle-manifest.js";
|
||||
import { normalizePluginsConfig, type NormalizedPluginsConfig } from "./config-state.js";
|
||||
import { discoverOpenClawPlugins, type PluginCandidate } from "./discovery.js";
|
||||
import { loadPluginManifest, type PluginManifest } from "./manifest.js";
|
||||
import {
|
||||
loadPluginManifest,
|
||||
type PluginManifest,
|
||||
type PluginManifestContracts,
|
||||
} from "./manifest.js";
|
||||
import { checkMinHostVersion } from "./min-host-version.js";
|
||||
import { isPathInside, safeRealpathSync } from "./path-safety.js";
|
||||
import { resolvePluginCacheInputs } from "./roots.js";
|
||||
@@ -64,6 +68,7 @@ export type PluginManifestRecord = {
|
||||
schemaCacheKey?: string;
|
||||
configSchema?: Record<string, unknown>;
|
||||
configUiHints?: Record<string, PluginConfigUiHint>;
|
||||
contracts?: PluginManifestContracts;
|
||||
channelCatalogMeta?: {
|
||||
id: string;
|
||||
preferOver?: string[];
|
||||
@@ -195,6 +200,7 @@ function buildRecord(params: {
|
||||
schemaCacheKey: params.schemaCacheKey,
|
||||
configSchema: params.configSchema,
|
||||
configUiHints: params.manifest.uiHints,
|
||||
contracts: params.manifest.contracts,
|
||||
...(params.candidate.packageManifest?.channel?.id
|
||||
? {
|
||||
channelCatalogMeta: {
|
||||
|
||||
@@ -32,6 +32,19 @@ export type PluginManifest = {
|
||||
description?: string;
|
||||
version?: string;
|
||||
uiHints?: Record<string, PluginConfigUiHint>;
|
||||
/**
|
||||
* Static capability ownership snapshot used for manifest-driven discovery,
|
||||
* compat wiring, and contract coverage without importing plugin runtime.
|
||||
*/
|
||||
contracts?: PluginManifestContracts;
|
||||
};
|
||||
|
||||
export type PluginManifestContracts = {
|
||||
speechProviders?: string[];
|
||||
mediaUnderstandingProviders?: string[];
|
||||
imageGenerationProviders?: string[];
|
||||
webSearchProviders?: string[];
|
||||
tools?: string[];
|
||||
};
|
||||
|
||||
export type PluginManifestProviderAuthChoice = {
|
||||
@@ -92,6 +105,30 @@ function normalizeStringListRecord(value: unknown): Record<string, string[]> | u
|
||||
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function normalizeManifestContracts(value: unknown): PluginManifestContracts | undefined {
|
||||
if (!isRecord(value)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const contracts = {
|
||||
...(normalizeStringList(value.speechProviders)
|
||||
? { speechProviders: normalizeStringList(value.speechProviders) }
|
||||
: {}),
|
||||
...(normalizeStringList(value.mediaUnderstandingProviders)
|
||||
? { mediaUnderstandingProviders: normalizeStringList(value.mediaUnderstandingProviders) }
|
||||
: {}),
|
||||
...(normalizeStringList(value.imageGenerationProviders)
|
||||
? { imageGenerationProviders: normalizeStringList(value.imageGenerationProviders) }
|
||||
: {}),
|
||||
...(normalizeStringList(value.webSearchProviders)
|
||||
? { webSearchProviders: normalizeStringList(value.webSearchProviders) }
|
||||
: {}),
|
||||
...(normalizeStringList(value.tools) ? { tools: normalizeStringList(value.tools) } : {}),
|
||||
} satisfies PluginManifestContracts;
|
||||
|
||||
return Object.keys(contracts).length > 0 ? contracts : undefined;
|
||||
}
|
||||
|
||||
function normalizeProviderAuthChoices(
|
||||
value: unknown,
|
||||
): PluginManifestProviderAuthChoice[] | undefined {
|
||||
@@ -215,6 +252,7 @@ export function loadPluginManifest(
|
||||
const providerAuthEnvVars = normalizeStringListRecord(raw.providerAuthEnvVars);
|
||||
const providerAuthChoices = normalizeProviderAuthChoices(raw.providerAuthChoices);
|
||||
const skills = normalizeStringList(raw.skills);
|
||||
const contracts = normalizeManifestContracts(raw.contracts);
|
||||
|
||||
let uiHints: Record<string, PluginConfigUiHint> | undefined;
|
||||
if (isRecord(raw.uiHints)) {
|
||||
@@ -241,6 +279,7 @@ export function loadPluginManifest(
|
||||
description,
|
||||
version,
|
||||
uiHints,
|
||||
contracts,
|
||||
},
|
||||
manifestPath,
|
||||
};
|
||||
|
||||
@@ -41,12 +41,16 @@ function buildWebSearchSnapshotCacheKey(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
bundledAllowlistCompat?: boolean;
|
||||
onlyPluginIds?: readonly string[];
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): string {
|
||||
const effectiveVitest = params.env.VITEST ?? process.env.VITEST ?? "";
|
||||
return JSON.stringify({
|
||||
workspaceDir: params.workspaceDir ?? "",
|
||||
bundledAllowlistCompat: params.bundledAllowlistCompat === true,
|
||||
onlyPluginIds: [...new Set(params.onlyPluginIds ?? [])].toSorted((left, right) =>
|
||||
left.localeCompare(right),
|
||||
),
|
||||
config: params.config ?? null,
|
||||
env: buildPluginSnapshotCacheEnvKey(params.env, {
|
||||
includeProcessVitestFallback: effectiveVitest !== (params.env.VITEST ?? ""),
|
||||
@@ -55,6 +59,9 @@ function buildWebSearchSnapshotCacheKey(params: {
|
||||
}
|
||||
|
||||
function pluginManifestDeclaresWebSearch(record: PluginManifestRecord): boolean {
|
||||
if ((record.contracts?.webSearchProviders?.length ?? 0) > 0) {
|
||||
return true;
|
||||
}
|
||||
const configUiHintKeys = Object.keys(record.configUiHints ?? {});
|
||||
if (configUiHintKeys.some((key) => key === "webSearch" || key.startsWith("webSearch."))) {
|
||||
return true;
|
||||
@@ -70,14 +77,21 @@ function resolveWebSearchCandidatePluginIds(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
onlyPluginIds?: readonly string[];
|
||||
}): string[] | undefined {
|
||||
const registry = loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
const onlyPluginIdSet =
|
||||
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
|
||||
const ids = registry.plugins
|
||||
.filter(pluginManifestDeclaresWebSearch)
|
||||
.filter(
|
||||
(plugin) =>
|
||||
pluginManifestDeclaresWebSearch(plugin) &&
|
||||
(!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)),
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
return ids.length > 0 ? ids : undefined;
|
||||
@@ -88,6 +102,7 @@ export function resolvePluginWebSearchProviders(params: {
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
bundledAllowlistCompat?: boolean;
|
||||
onlyPluginIds?: readonly string[];
|
||||
activate?: boolean;
|
||||
cache?: boolean;
|
||||
}): PluginWebSearchProviderEntry[] {
|
||||
@@ -99,6 +114,7 @@ export function resolvePluginWebSearchProviders(params: {
|
||||
config: cacheOwnerConfig,
|
||||
workspaceDir: params.workspaceDir,
|
||||
bundledAllowlistCompat: params.bundledAllowlistCompat,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
env,
|
||||
});
|
||||
if (cacheOwnerConfig && shouldMemoizeSnapshot) {
|
||||
@@ -117,6 +133,7 @@ export function resolvePluginWebSearchProviders(params: {
|
||||
config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
});
|
||||
const registry = loadOpenClawPlugins({
|
||||
config,
|
||||
@@ -162,14 +179,19 @@ export function resolveRuntimeWebSearchProviders(params: {
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
bundledAllowlistCompat?: boolean;
|
||||
onlyPluginIds?: readonly string[];
|
||||
}): PluginWebSearchProviderEntry[] {
|
||||
const runtimeProviders = getActivePluginRegistry()?.webSearchProviders ?? [];
|
||||
const onlyPluginIdSet =
|
||||
params.onlyPluginIds && params.onlyPluginIds.length > 0 ? new Set(params.onlyPluginIds) : null;
|
||||
if (runtimeProviders.length > 0) {
|
||||
return sortWebSearchProviders(
|
||||
runtimeProviders.map((entry) => ({
|
||||
...entry.provider,
|
||||
pluginId: entry.pluginId,
|
||||
})),
|
||||
runtimeProviders
|
||||
.filter((entry) => !onlyPluginIdSet || onlyPluginIdSet.has(entry.pluginId))
|
||||
.map((entry) => ({
|
||||
...entry.provider,
|
||||
pluginId: entry.pluginId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
return resolvePluginWebSearchProviders(params);
|
||||
|
||||
@@ -96,7 +96,7 @@ describe("resolveBundledPluginWebSearchProviders", () => {
|
||||
expect(providers).toEqual([]);
|
||||
});
|
||||
|
||||
it("can resolve bundled providers without the plugin loader", () => {
|
||||
it("can resolve bundled providers through the manifest-scoped loader path", () => {
|
||||
const providers = resolveBundledPluginWebSearchProviders({
|
||||
bundledAllowlistCompat: true,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user