mirror of https://github.com/openclaw/openclaw.git
feat: add Fireworks provider and simplify plugin setup loading
This commit is contained in:
parent
f842f518cd
commit
d655a8bc76
|
|
@ -11,6 +11,11 @@ Docs: https://docs.openclaw.ai
|
|||
### Changes
|
||||
|
||||
- Memory/dreaming (experimental): add weighted short-term recall promotion, managed dreaming modes (`off|core|rem|deep`), a `/dreaming` command, Dreams UI, multilingual conceptual tagging, and doctor/status repair support so durable memory promotion can run in the background with less manual setup. (#60569, #60697)
|
||||
- Channels/context visibility: add configurable `contextVisibility` per channel (`all`, `allowlist`, `allowlist_quote`) so supplemental quote, thread, and fetched history context can be filtered by sender allowlists instead of always passing through as received.
|
||||
- Matrix/exec approvals: add Matrix-native exec approval prompts with account-scoped approvers, channel-or-DM delivery, and room-thread aware resolution handling. (#58635) Thanks @gumadeiras.
|
||||
- Providers/StepFun: add the bundled StepFun provider plugin with standard and Step Plan endpoints, China/global onboarding choices, `step-3.5-flash` on both catalogs, and `step-3.5-flash-2603` currently exposed on Step Plan. (#60032) Thanks @hengm3467.
|
||||
- Providers/Fireworks: add a bundled Fireworks AI provider plugin with `FIREWORKS_API_KEY` onboarding, Fire Pass Kimi defaults, and dynamic Fireworks model-id support.
|
||||
- Providers/config: add `models.providers.*.request` overrides for headers and auth on model-provider paths, and full request transport overrides for media provider HTTP paths.
|
||||
- MiniMax/TTS: add a bundled MiniMax speech provider backed by the T2A v2 API so speech synthesis can run through MiniMax-native voices and auth. (#55921) Thanks @duncanita.
|
||||
- Control UI/skills: add ClawHub search, detail, and install flows directly in the Skills panel. (#60134) Thanks @samzong.
|
||||
- Providers/Ollama: add a bundled Ollama Web Search provider for key-free `web_search` via your configured Ollama host and `ollama signin`. (#59318) Thanks @BruceMacD.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
summary: "Fireworks setup (auth + model selection)"
|
||||
read_when:
|
||||
- You want to use Fireworks with OpenClaw
|
||||
- You need the Fireworks API key env var or default model id
|
||||
---
|
||||
|
||||
# Fireworks
|
||||
|
||||
[Fireworks](https://fireworks.ai) exposes open-weight and routed models through an OpenAI-compatible API. OpenClaw now includes a bundled Fireworks provider plugin.
|
||||
|
||||
- Provider: `fireworks`
|
||||
- Auth: `FIREWORKS_API_KEY`
|
||||
- API: OpenAI-compatible chat/completions
|
||||
- Base URL: `https://api.fireworks.ai/inference/v1`
|
||||
- Default model: `fireworks/accounts/fireworks/routers/kimi-k2p5-turbo`
|
||||
|
||||
## Quick start
|
||||
|
||||
Set up Fireworks auth through onboarding:
|
||||
|
||||
```bash
|
||||
openclaw onboard --auth-choice fireworks-api-key
|
||||
```
|
||||
|
||||
This stores your Fireworks key in OpenClaw config and sets the Fire Pass starter model as the default.
|
||||
|
||||
## Non-interactive example
|
||||
|
||||
```bash
|
||||
openclaw onboard --non-interactive \
|
||||
--mode local \
|
||||
--auth-choice fireworks-api-key \
|
||||
--fireworks-api-key "$FIREWORKS_API_KEY" \
|
||||
--skip-health \
|
||||
--accept-risk
|
||||
```
|
||||
|
||||
## Environment note
|
||||
|
||||
If the Gateway runs outside your interactive shell, make sure `FIREWORKS_API_KEY`
|
||||
is available to that process too. A key sitting only in `~/.profile` will not
|
||||
help a launchd/systemd daemon unless that environment is imported there as well.
|
||||
|
||||
## Built-in catalog
|
||||
|
||||
| Model ref | Name | Input | Context | Max output | Notes |
|
||||
| ------------------------------------------------------ | --------------------------- | ---------- | ------- | ---------- | ------------------------------------------ |
|
||||
| `fireworks/accounts/fireworks/routers/kimi-k2p5-turbo` | Kimi K2.5 Turbo (Fire Pass) | text,image | 256,000 | 256,000 | Default bundled starter model on Fireworks |
|
||||
|
||||
## Custom Fireworks model ids
|
||||
|
||||
OpenClaw accepts dynamic Fireworks model ids too. Use the exact model or router id shown by Fireworks and prefix it with `fireworks/`.
|
||||
|
||||
Example:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
If Fireworks publishes a newer model such as a fresh Qwen or Gemma release, you can switch to it directly by using its Fireworks model id without waiting for a bundled catalog update.
|
||||
|
|
@ -32,6 +32,7 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi
|
|||
- [Chutes](/providers/chutes)
|
||||
- [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway)
|
||||
- [DeepSeek](/providers/deepseek)
|
||||
- [Fireworks](/providers/fireworks)
|
||||
- [GitHub Copilot](/providers/github-copilot)
|
||||
- [GLM models](/providers/glm)
|
||||
- [Google (Gemini)](/providers/google)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ model as `provider/model`.
|
|||
- [BytePlus (International)](/concepts/model-providers#byteplus-international)
|
||||
- [Chutes](/providers/chutes)
|
||||
- [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway)
|
||||
- [Fireworks](/providers/fireworks)
|
||||
- [GLM models](/providers/glm)
|
||||
- [MiniMax](/providers/minimax)
|
||||
- [Mistral](/providers/mistral)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
import type { ModelRegistry } from "@mariozechner/pi-coding-agent";
|
||||
import type {
|
||||
ProviderResolveDynamicModelContext,
|
||||
ProviderRuntimeModel,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveProviderPluginChoice } from "../../src/plugins/provider-auth-choice.runtime.js";
|
||||
import { registerSingleProviderPlugin } from "../../test/helpers/plugins/plugin-registration.js";
|
||||
import fireworksPlugin from "./index.js";
|
||||
import {
|
||||
FIREWORKS_BASE_URL,
|
||||
FIREWORKS_DEFAULT_CONTEXT_WINDOW,
|
||||
FIREWORKS_DEFAULT_MAX_TOKENS,
|
||||
FIREWORKS_DEFAULT_MODEL_ID,
|
||||
} from "./provider-catalog.js";
|
||||
|
||||
function createDynamicContext(params: {
|
||||
provider: string;
|
||||
modelId: string;
|
||||
models: ProviderRuntimeModel[];
|
||||
}): ProviderResolveDynamicModelContext {
|
||||
return {
|
||||
provider: params.provider,
|
||||
modelId: params.modelId,
|
||||
modelRegistry: {
|
||||
find(providerId: string, modelId: string) {
|
||||
return (
|
||||
params.models.find(
|
||||
(model) =>
|
||||
model.provider === providerId && model.id.toLowerCase() === modelId.toLowerCase(),
|
||||
) ?? null
|
||||
);
|
||||
},
|
||||
} as ModelRegistry,
|
||||
};
|
||||
}
|
||||
|
||||
describe("fireworks provider plugin", () => {
|
||||
it("registers Fireworks with api-key auth wizard metadata", async () => {
|
||||
const provider = await registerSingleProviderPlugin(fireworksPlugin);
|
||||
const resolved = resolveProviderPluginChoice({
|
||||
providers: [provider],
|
||||
choice: "fireworks-api-key",
|
||||
});
|
||||
|
||||
expect(provider.id).toBe("fireworks");
|
||||
expect(provider.label).toBe("Fireworks");
|
||||
expect(provider.aliases).toEqual(["fireworks-ai"]);
|
||||
expect(provider.envVars).toEqual(["FIREWORKS_API_KEY"]);
|
||||
expect(provider.auth).toHaveLength(1);
|
||||
expect(resolved?.provider.id).toBe("fireworks");
|
||||
expect(resolved?.method.id).toBe("api-key");
|
||||
});
|
||||
|
||||
it("builds the Fireworks Fire Pass starter catalog", async () => {
|
||||
const provider = await registerSingleProviderPlugin(fireworksPlugin);
|
||||
const catalog = await provider.catalog?.run({
|
||||
config: {},
|
||||
env: {},
|
||||
resolveProviderApiKey: () => ({ apiKey: "test-key" }),
|
||||
resolveProviderAuth: () => ({
|
||||
apiKey: "test-key",
|
||||
mode: "api_key",
|
||||
source: "env",
|
||||
}),
|
||||
} as never);
|
||||
|
||||
expect(catalog && "provider" in catalog).toBe(true);
|
||||
if (!catalog || !("provider" in catalog)) {
|
||||
throw new Error("expected single-provider catalog");
|
||||
}
|
||||
|
||||
expect(catalog.provider.api).toBe("openai-completions");
|
||||
expect(catalog.provider.baseUrl).toBe(FIREWORKS_BASE_URL);
|
||||
expect(catalog.provider.models?.map((model) => model.id)).toEqual([FIREWORKS_DEFAULT_MODEL_ID]);
|
||||
expect(catalog.provider.models?.[0]).toMatchObject({
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: FIREWORKS_DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: FIREWORKS_DEFAULT_MAX_TOKENS,
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves forward-compat Fireworks model ids from the default template", async () => {
|
||||
const provider = await registerSingleProviderPlugin(fireworksPlugin);
|
||||
const resolved = provider.resolveDynamicModel?.(
|
||||
createDynamicContext({
|
||||
provider: "fireworks",
|
||||
modelId: "accounts/fireworks/models/qwen3.6-plus",
|
||||
models: [
|
||||
{
|
||||
id: FIREWORKS_DEFAULT_MODEL_ID,
|
||||
name: FIREWORKS_DEFAULT_MODEL_ID,
|
||||
provider: "fireworks",
|
||||
api: "openai-completions",
|
||||
baseUrl: FIREWORKS_BASE_URL,
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: FIREWORKS_DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: FIREWORKS_DEFAULT_MAX_TOKENS,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(resolved).toMatchObject({
|
||||
provider: "fireworks",
|
||||
id: "accounts/fireworks/models/qwen3.6-plus",
|
||||
api: "openai-completions",
|
||||
baseUrl: FIREWORKS_BASE_URL,
|
||||
reasoning: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import type {
|
||||
ProviderResolveDynamicModelContext,
|
||||
ProviderRuntimeModel,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
|
||||
import {
|
||||
buildProviderReplayFamilyHooks,
|
||||
cloneFirstTemplateModel,
|
||||
DEFAULT_CONTEXT_TOKENS,
|
||||
normalizeModelCompat,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { applyFireworksConfig, FIREWORKS_DEFAULT_MODEL_REF } from "./onboard.js";
|
||||
import {
|
||||
buildFireworksProvider,
|
||||
FIREWORKS_BASE_URL,
|
||||
FIREWORKS_DEFAULT_CONTEXT_WINDOW,
|
||||
FIREWORKS_DEFAULT_MAX_TOKENS,
|
||||
FIREWORKS_DEFAULT_MODEL_ID,
|
||||
} from "./provider-catalog.js";
|
||||
|
||||
const PROVIDER_ID = "fireworks";
|
||||
const OPENAI_COMPATIBLE_REPLAY_HOOKS = buildProviderReplayFamilyHooks({
|
||||
family: "openai-compatible",
|
||||
});
|
||||
|
||||
function resolveFireworksDynamicModel(
|
||||
ctx: ProviderResolveDynamicModelContext,
|
||||
): ProviderRuntimeModel | undefined {
|
||||
const modelId = ctx.modelId.trim();
|
||||
if (!modelId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
cloneFirstTemplateModel({
|
||||
providerId: PROVIDER_ID,
|
||||
modelId,
|
||||
templateIds: [FIREWORKS_DEFAULT_MODEL_ID],
|
||||
ctx,
|
||||
patch: {
|
||||
provider: PROVIDER_ID,
|
||||
},
|
||||
}) ??
|
||||
normalizeModelCompat({
|
||||
id: modelId,
|
||||
name: modelId,
|
||||
provider: PROVIDER_ID,
|
||||
api: "openai-completions",
|
||||
baseUrl: FIREWORKS_BASE_URL,
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: FIREWORKS_DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: FIREWORKS_DEFAULT_MAX_TOKENS || DEFAULT_CONTEXT_TOKENS,
|
||||
} as ProviderRuntimeModel)
|
||||
);
|
||||
}
|
||||
|
||||
export default defineSingleProviderPluginEntry({
|
||||
id: PROVIDER_ID,
|
||||
name: "Fireworks Provider",
|
||||
description: "Bundled Fireworks AI provider plugin",
|
||||
provider: {
|
||||
label: "Fireworks",
|
||||
aliases: ["fireworks-ai"],
|
||||
docsPath: "/providers/fireworks",
|
||||
auth: [
|
||||
{
|
||||
methodId: "api-key",
|
||||
label: "Fireworks API key",
|
||||
hint: "API key",
|
||||
optionKey: "fireworksApiKey",
|
||||
flagName: "--fireworks-api-key",
|
||||
envVar: "FIREWORKS_API_KEY",
|
||||
promptMessage: "Enter Fireworks API key",
|
||||
defaultModel: FIREWORKS_DEFAULT_MODEL_REF,
|
||||
applyConfig: (cfg) => applyFireworksConfig(cfg),
|
||||
},
|
||||
],
|
||||
catalog: {
|
||||
buildProvider: buildFireworksProvider,
|
||||
allowExplicitBaseUrl: true,
|
||||
},
|
||||
...OPENAI_COMPATIBLE_REPLAY_HOOKS,
|
||||
resolveDynamicModel: (ctx) => resolveFireworksDynamicModel(ctx),
|
||||
isModernModelRef: () => true,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import {
|
||||
createDefaultModelsPresetAppliers,
|
||||
type OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/provider-onboard";
|
||||
import {
|
||||
buildFireworksCatalogModels,
|
||||
buildFireworksProvider,
|
||||
FIREWORKS_DEFAULT_MODEL_ID,
|
||||
} from "./provider-catalog.js";
|
||||
|
||||
export const FIREWORKS_DEFAULT_MODEL_REF = `fireworks/${FIREWORKS_DEFAULT_MODEL_ID}`;
|
||||
|
||||
const fireworksPresetAppliers = createDefaultModelsPresetAppliers({
|
||||
primaryModelRef: FIREWORKS_DEFAULT_MODEL_REF,
|
||||
resolveParams: (_cfg: OpenClawConfig) => {
|
||||
const defaultProvider = buildFireworksProvider();
|
||||
return {
|
||||
providerId: "fireworks",
|
||||
api: defaultProvider.api ?? "openai-completions",
|
||||
baseUrl: defaultProvider.baseUrl,
|
||||
defaultModels: buildFireworksCatalogModels(),
|
||||
defaultModelId: FIREWORKS_DEFAULT_MODEL_ID,
|
||||
aliases: [{ modelRef: FIREWORKS_DEFAULT_MODEL_REF, alias: "Kimi K2.5 Turbo" }],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export function applyFireworksProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return fireworksPresetAppliers.applyProviderConfig(cfg);
|
||||
}
|
||||
|
||||
export function applyFireworksConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return fireworksPresetAppliers.applyConfig(cfg);
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"id": "fireworks",
|
||||
"enabledByDefault": true,
|
||||
"providers": ["fireworks"],
|
||||
"providerAuthEnvVars": {
|
||||
"fireworks": ["FIREWORKS_API_KEY"]
|
||||
},
|
||||
"providerAuthChoices": [
|
||||
{
|
||||
"provider": "fireworks",
|
||||
"method": "api-key",
|
||||
"choiceId": "fireworks-api-key",
|
||||
"choiceLabel": "Fireworks API key",
|
||||
"groupId": "fireworks",
|
||||
"groupLabel": "Fireworks",
|
||||
"groupHint": "API key",
|
||||
"optionKey": "fireworksApiKey",
|
||||
"cliFlag": "--fireworks-api-key",
|
||||
"cliOption": "--fireworks-api-key <key>",
|
||||
"cliDescription": "Fireworks API key"
|
||||
}
|
||||
],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "@openclaw/fireworks-provider",
|
||||
"version": "2026.4.1-beta.1",
|
||||
"private": true,
|
||||
"description": "OpenClaw Fireworks provider plugin",
|
||||
"type": "module",
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import type {
|
||||
ModelDefinitionConfig,
|
||||
ModelProviderConfig,
|
||||
} from "openclaw/plugin-sdk/provider-model-shared";
|
||||
|
||||
export const FIREWORKS_BASE_URL = "https://api.fireworks.ai/inference/v1";
|
||||
export const FIREWORKS_DEFAULT_MODEL_ID = "accounts/fireworks/routers/kimi-k2p5-turbo";
|
||||
export const FIREWORKS_DEFAULT_CONTEXT_WINDOW = 256000;
|
||||
export const FIREWORKS_DEFAULT_MAX_TOKENS = 256000;
|
||||
|
||||
const ZERO_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
} as const;
|
||||
|
||||
export function buildFireworksCatalogModels(): ModelDefinitionConfig[] {
|
||||
return [
|
||||
{
|
||||
id: FIREWORKS_DEFAULT_MODEL_ID,
|
||||
name: "Kimi K2.5 Turbo (Fire Pass)",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: ZERO_COST,
|
||||
contextWindow: FIREWORKS_DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: FIREWORKS_DEFAULT_MAX_TOKENS,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function buildFireworksProvider(): ModelProviderConfig {
|
||||
return {
|
||||
baseUrl: FIREWORKS_BASE_URL,
|
||||
api: "openai-completions",
|
||||
models: buildFireworksCatalogModels(),
|
||||
};
|
||||
}
|
||||
|
|
@ -1036,7 +1036,6 @@
|
|||
"check": "pnpm check:no-conflict-markers && pnpm check:host-env-policy:swift && pnpm tsgo && pnpm lint && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope",
|
||||
"check:base-config-schema": "node --import tsx scripts/generate-base-config-schema.ts --check",
|
||||
"check:bundled-channel-config-metadata": "node --import tsx scripts/generate-bundled-channel-config-metadata.ts --check",
|
||||
"check:bundled-provider-auth-env-vars": "node scripts/generate-bundled-provider-auth-env-vars.mjs --check",
|
||||
"check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-i18n-glossary && pnpm docs:check-links",
|
||||
"check:host-env-policy:swift": "node scripts/generate-host-env-security-policy-swift.mjs --check",
|
||||
"check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500",
|
||||
|
|
@ -1132,7 +1131,7 @@
|
|||
"protocol:check": "pnpm protocol:gen && pnpm protocol:gen:swift && git diff --exit-code -- dist/protocol.schema.json apps/macos/Sources/OpenClawProtocol/GatewayModels.swift apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift",
|
||||
"protocol:gen": "node --import tsx scripts/protocol-gen.ts",
|
||||
"protocol:gen:swift": "node --import tsx scripts/protocol-gen-swift.ts",
|
||||
"release:check": "pnpm check:base-config-schema && pnpm check:bundled-channel-config-metadata && pnpm check:bundled-provider-auth-env-vars && pnpm config:docs:check && pnpm plugin-sdk:check-exports && pnpm plugin-sdk:facades:check && pnpm plugin-sdk:api:check && node --import tsx scripts/release-check.ts",
|
||||
"release:check": "pnpm check:base-config-schema && pnpm check:bundled-channel-config-metadata && pnpm config:docs:check && pnpm plugin-sdk:check-exports && pnpm plugin-sdk:facades:check && pnpm plugin-sdk:api:check && node --import tsx scripts/release-check.ts",
|
||||
"release:openclaw:npm:check": "node --import tsx scripts/openclaw-npm-release-check.ts",
|
||||
"release:openclaw:npm:verify-published": "node --import tsx scripts/openclaw-npm-postpublish-verify.ts",
|
||||
"release:plugins:clawhub:check": "node --import tsx scripts/plugin-clawhub-release-check.ts",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ const allowedFiles = new Set([
|
|||
"src/agents/tools/web-fetch.test-harness.ts",
|
||||
"src/config/legacy-web-fetch.ts",
|
||||
"src/config/zod-schema.agent-runtime.ts",
|
||||
"src/plugins/bundled-provider-auth-env-vars.generated.ts",
|
||||
"src/secrets/target-registry-data.ts",
|
||||
]);
|
||||
const suspiciousPatterns = [
|
||||
|
|
|
|||
|
|
@ -1,83 +0,0 @@
|
|||
import path from "node:path";
|
||||
import { collectBundledPluginSources } from "./lib/bundled-plugin-source-utils.mjs";
|
||||
import { reportGeneratedOutputCli, writeGeneratedOutput } from "./lib/generated-output-utils.mjs";
|
||||
|
||||
const GENERATED_BY = "scripts/generate-bundled-provider-auth-env-vars.mjs";
|
||||
const DEFAULT_OUTPUT_PATH = "src/plugins/bundled-provider-auth-env-vars.generated.ts";
|
||||
|
||||
function normalizeProviderAuthEnvVars(providerAuthEnvVars) {
|
||||
if (
|
||||
!providerAuthEnvVars ||
|
||||
typeof providerAuthEnvVars !== "object" ||
|
||||
Array.isArray(providerAuthEnvVars)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(providerAuthEnvVars)
|
||||
.map(([providerId, envVars]) => {
|
||||
const normalizedProviderId = providerId.trim();
|
||||
const normalizedEnvVars = Array.isArray(envVars)
|
||||
? envVars.map((value) => String(value).trim()).filter(Boolean)
|
||||
: [];
|
||||
if (!normalizedProviderId || normalizedEnvVars.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return [normalizedProviderId, normalizedEnvVars];
|
||||
})
|
||||
.filter(Boolean)
|
||||
.toSorted(([left], [right]) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function collectBundledProviderAuthEnvVars(params = {}) {
|
||||
const repoRoot = path.resolve(params.repoRoot ?? process.cwd());
|
||||
const entries = new Map();
|
||||
for (const source of collectBundledPluginSources({ repoRoot })) {
|
||||
for (const [providerId, envVars] of normalizeProviderAuthEnvVars(
|
||||
source.manifest.providerAuthEnvVars,
|
||||
)) {
|
||||
entries.set(providerId, envVars);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
[...entries.entries()].toSorted(([left], [right]) => left.localeCompare(right)),
|
||||
);
|
||||
}
|
||||
|
||||
export function renderBundledProviderAuthEnvVarModule(entries) {
|
||||
const renderedEntries = Object.entries(entries)
|
||||
.map(([providerId, envVars]) => {
|
||||
const renderedKey = /^[$A-Z_a-z][\w$]*$/u.test(providerId)
|
||||
? providerId
|
||||
: JSON.stringify(providerId);
|
||||
const renderedEnvVars = envVars.map((value) => JSON.stringify(value)).join(", ");
|
||||
return ` ${renderedKey}: [${renderedEnvVars}],`;
|
||||
})
|
||||
.join("\n");
|
||||
return `// Auto-generated by ${GENERATED_BY}. Do not edit directly.
|
||||
|
||||
export const BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES = {
|
||||
${renderedEntries}
|
||||
} as const satisfies Record<string, readonly string[]>;
|
||||
`;
|
||||
}
|
||||
|
||||
export function writeBundledProviderAuthEnvVarModule(params = {}) {
|
||||
const repoRoot = path.resolve(params.repoRoot ?? process.cwd());
|
||||
const next = renderBundledProviderAuthEnvVarModule(
|
||||
collectBundledProviderAuthEnvVars({ repoRoot }),
|
||||
);
|
||||
return writeGeneratedOutput({
|
||||
repoRoot,
|
||||
outputPath: params.outputPath ?? DEFAULT_OUTPUT_PATH,
|
||||
next,
|
||||
check: params.check,
|
||||
});
|
||||
}
|
||||
|
||||
reportGeneratedOutputCli({
|
||||
importMetaUrl: import.meta.url,
|
||||
label: "bundled-provider-auth-env-vars",
|
||||
run: ({ check }) => writeBundledProviderAuthEnvVarModule({ check }),
|
||||
});
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
import {
|
||||
PROVIDER_AUTH_ENV_VAR_CANDIDATES,
|
||||
listKnownProviderAuthEnvVarNames,
|
||||
resolveProviderAuthEnvVarCandidates,
|
||||
} from "../secrets/provider-env-vars.js";
|
||||
|
||||
export const PROVIDER_ENV_API_KEY_CANDIDATES = PROVIDER_AUTH_ENV_VAR_CANDIDATES;
|
||||
export function resolveProviderEnvApiKeyCandidates(): Record<string, readonly string[]> {
|
||||
return resolveProviderAuthEnvVarCandidates();
|
||||
}
|
||||
|
||||
export const PROVIDER_ENV_API_KEY_CANDIDATES = resolveProviderEnvApiKeyCandidates();
|
||||
|
||||
export function listKnownProviderEnvApiKeyNames(): string[] {
|
||||
return listKnownProviderAuthEnvVarNames();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { getEnvApiKey } from "@mariozechner/pi-ai";
|
|||
import { hasAnthropicVertexAvailableAuth } from "../../extensions/anthropic-vertex/api.js";
|
||||
import { getShellEnvAppliedKeys } from "../infra/shell-env.js";
|
||||
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
|
||||
import { PROVIDER_ENV_API_KEY_CANDIDATES } from "./model-auth-env-vars.js";
|
||||
import { resolveProviderEnvApiKeyCandidates } from "./model-auth-env-vars.js";
|
||||
import { GCP_VERTEX_CREDENTIALS_MARKER } from "./model-auth-markers.js";
|
||||
import { normalizeProviderIdForAuth } from "./provider-id.js";
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ export function resolveEnvApiKey(
|
|||
return { apiKey: value, source };
|
||||
};
|
||||
|
||||
const candidates = PROVIDER_ENV_API_KEY_CANDIDATES[normalized];
|
||||
const candidates = resolveProviderEnvApiKeyCandidates()[normalized];
|
||||
if (candidates) {
|
||||
for (const envVar of candidates) {
|
||||
const resolved = pick(envVar);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from "../plugins/provider-runtime.js";
|
||||
import type { ProviderRuntimeModel } from "../plugins/types.js";
|
||||
import { ensureAuthProfileStore } from "./auth-profiles.js";
|
||||
import { PROVIDER_ENV_API_KEY_CANDIDATES } from "./model-auth-env-vars.js";
|
||||
import { resolveProviderEnvApiKeyCandidates } from "./model-auth-env-vars.js";
|
||||
import { resolveEnvApiKey } from "./model-auth-env.js";
|
||||
import { resolvePiCredentialMapFromStore, type PiCredentialMap } from "./pi-auth-credentials.js";
|
||||
|
||||
|
|
@ -232,7 +232,7 @@ function resolvePiCredentials(agentDir: string): PiCredentialMap {
|
|||
// pi-coding-agent hides providers from its registry when auth storage lacks
|
||||
// a matching credential entry. Mirror env-backed provider auth here so
|
||||
// live/model discovery sees the same providers runtime auth can use.
|
||||
for (const provider of Object.keys(PROVIDER_ENV_API_KEY_CANDIDATES)) {
|
||||
for (const provider of Object.keys(resolveProviderEnvApiKeyCandidates())) {
|
||||
if (credentials[provider]) {
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -899,7 +899,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
|||
attempts: [
|
||||
{
|
||||
provider: "fireworks",
|
||||
model: "fireworks/minimax-m2p5",
|
||||
model: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
error: "Provider fireworks is in cooldown (all profiles unavailable)",
|
||||
reason: "rate_limit",
|
||||
},
|
||||
|
|
@ -960,7 +960,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
|||
attempts: [
|
||||
{
|
||||
provider: "fireworks",
|
||||
model: "fireworks/minimax-m2p5",
|
||||
model: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
error: "Provider fireworks is in cooldown (all profiles unavailable)",
|
||||
reason: "rate_limit",
|
||||
},
|
||||
|
|
@ -1034,7 +1034,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
|||
attempts: [
|
||||
{
|
||||
provider: "fireworks",
|
||||
model: "fireworks/minimax-m2p5",
|
||||
model: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
error: "Provider fireworks is in cooldown (all profiles unavailable)",
|
||||
reason: "rate_limit",
|
||||
},
|
||||
|
|
@ -1097,7 +1097,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
|||
attempts: [
|
||||
{
|
||||
provider: "fireworks",
|
||||
model: "fireworks/minimax-m2p5",
|
||||
model: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
error: "Provider fireworks is in cooldown (all profiles unavailable)",
|
||||
reason: "rate_limit",
|
||||
},
|
||||
|
|
@ -1177,7 +1177,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
|||
attempts: [
|
||||
{
|
||||
provider: "fireworks",
|
||||
model: "fireworks/minimax-m2p5",
|
||||
model: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
error: "Provider fireworks is in cooldown (all profiles unavailable)",
|
||||
reason: "rate_limit",
|
||||
},
|
||||
|
|
@ -1299,7 +1299,7 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
|||
sessionId,
|
||||
updatedAt: Date.now(),
|
||||
sessionFile: transcriptPath,
|
||||
fallbackNoticeSelectedModel: "fireworks/minimax-m2p5",
|
||||
fallbackNoticeSelectedModel: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
fallbackNoticeActiveModel: "deepinfra/moonshotai/Kimi-K2.5",
|
||||
fallbackNoticeReason: "rate limit",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -207,16 +207,18 @@ describe("/model chat UX", () => {
|
|||
it("shows active runtime model when different from selected model", async () => {
|
||||
const reply = await resolveModelInfoReply({
|
||||
provider: "fireworks",
|
||||
model: "fireworks/minimax-m2p5",
|
||||
model: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
defaultProvider: "fireworks",
|
||||
defaultModel: "fireworks/minimax-m2p5",
|
||||
defaultModel: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
sessionEntry: {
|
||||
modelProvider: "deepinfra",
|
||||
model: "moonshotai/Kimi-K2.5",
|
||||
},
|
||||
});
|
||||
|
||||
expect(reply?.text).toContain("Current: fireworks/minimax-m2p5 (selected)");
|
||||
expect(reply?.text).toContain(
|
||||
"Current: fireworks/accounts/fireworks/routers/kimi-k2p5-turbo (selected)",
|
||||
);
|
||||
expect(reply?.text).toContain("Active: deepinfra/moonshotai/Kimi-K2.5 (runtime)");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -746,7 +746,7 @@ describe("buildStatusMessage", () => {
|
|||
updatedAt: 0,
|
||||
modelProvider: "anthropic",
|
||||
model: "claude-haiku-4-5",
|
||||
fallbackNoticeSelectedModel: "fireworks/minimax-m2p5",
|
||||
fallbackNoticeSelectedModel: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
fallbackNoticeActiveModel: "deepinfra/moonshotai/Kimi-K2.5",
|
||||
fallbackNoticeReason: "rate limit",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,11 +15,66 @@ const resolveManifestProviderAuthChoices = vi.hoisted(() =>
|
|||
const resolveProviderWizardOptions = vi.hoisted(() =>
|
||||
vi.fn<() => ProviderWizardOption[]>(() => []),
|
||||
);
|
||||
vi.mock("../plugins/provider-auth-choices.js", () => ({
|
||||
resolveManifestProviderAuthChoices,
|
||||
}));
|
||||
vi.mock("../plugins/provider-wizard.js", () => ({
|
||||
resolveProviderWizardOptions,
|
||||
|
||||
function includesOnboardingScope(
|
||||
scopes: readonly ("text-inference" | "image-generation")[] | undefined,
|
||||
scope: "text-inference" | "image-generation",
|
||||
): boolean {
|
||||
return scopes ? scopes.includes(scope) : scope === "text-inference";
|
||||
}
|
||||
|
||||
vi.mock("../flows/provider-flow.js", () => ({
|
||||
resolveProviderSetupFlowContributions: vi.fn(
|
||||
(params?: { scope?: "text-inference" | "image-generation" }) => {
|
||||
const scope = params?.scope ?? "text-inference";
|
||||
return [
|
||||
...resolveManifestProviderAuthChoices()
|
||||
.filter((choice) => includesOnboardingScope(choice.onboardingScopes, scope))
|
||||
.map((choice) => ({
|
||||
option: {
|
||||
value: choice.choiceId,
|
||||
label: choice.choiceLabel,
|
||||
...(choice.choiceHint ? { hint: choice.choiceHint } : {}),
|
||||
...(choice.groupId && choice.groupLabel
|
||||
? {
|
||||
group: {
|
||||
id: choice.groupId,
|
||||
label: choice.groupLabel,
|
||||
...(choice.groupHint ? { hint: choice.groupHint } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(choice.assistantPriority !== undefined
|
||||
? { assistantPriority: choice.assistantPriority }
|
||||
: {}),
|
||||
...(choice.assistantVisibility
|
||||
? { assistantVisibility: choice.assistantVisibility }
|
||||
: {}),
|
||||
},
|
||||
})),
|
||||
...resolveProviderWizardOptions()
|
||||
.filter((option) => includesOnboardingScope(option.onboardingScopes, scope))
|
||||
.map((option) => ({
|
||||
option: {
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
...(option.hint ? { hint: option.hint } : {}),
|
||||
group: {
|
||||
id: option.groupId,
|
||||
label: option.groupLabel,
|
||||
...(option.groupHint ? { hint: option.groupHint } : {}),
|
||||
},
|
||||
...(option.assistantPriority !== undefined
|
||||
? { assistantPriority: option.assistantPriority }
|
||||
: {}),
|
||||
...(option.assistantVisibility
|
||||
? { assistantVisibility: option.assistantVisibility }
|
||||
: {}),
|
||||
},
|
||||
})),
|
||||
];
|
||||
},
|
||||
),
|
||||
}));
|
||||
|
||||
const EMPTY_STORE: AuthProfileStore = { version: 1, profiles: {} };
|
||||
|
|
@ -212,7 +267,7 @@ describe("buildAuthChoiceOptions", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("builds cli help choices from the same catalog", () => {
|
||||
it("builds cli help choices from the same runtime catalog", () => {
|
||||
resolveManifestProviderAuthChoices.mockReturnValue([
|
||||
{
|
||||
pluginId: "chutes",
|
||||
|
|
@ -257,7 +312,7 @@ describe("buildAuthChoiceOptions", () => {
|
|||
expect(cliChoices).toContain("custom-api-key");
|
||||
expect(cliChoices).toContain("skip");
|
||||
expect(options.some((option) => option.value === "ollama")).toBe(true);
|
||||
expect(cliChoices).not.toContain("ollama");
|
||||
expect(cliChoices).toContain("ollama");
|
||||
});
|
||||
|
||||
it("can include legacy aliases in cli help choices", () => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import type { AuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
resolveManifestProviderSetupFlowContributions,
|
||||
resolveProviderSetupFlowContributions,
|
||||
} from "../flows/provider-flow.js";
|
||||
import { resolveProviderSetupFlowContributions } from "../flows/provider-flow.js";
|
||||
import {
|
||||
CORE_AUTH_CHOICE_OPTIONS,
|
||||
type AuthChoiceGroup,
|
||||
|
|
@ -38,6 +35,12 @@ function resolveProviderChoiceOptions(params?: {
|
|||
value: contribution.option.value as AuthChoice,
|
||||
label: contribution.option.label,
|
||||
...(contribution.option.hint ? { hint: contribution.option.hint } : {}),
|
||||
...(contribution.option.assistantPriority !== undefined
|
||||
? { assistantPriority: contribution.option.assistantPriority }
|
||||
: {}),
|
||||
...(contribution.option.assistantVisibility
|
||||
? { assistantVisibility: contribution.option.assistantVisibility }
|
||||
: {}),
|
||||
...(contribution.option.group
|
||||
? {
|
||||
groupId: contribution.option.group.id as AuthChoiceGroupId,
|
||||
|
|
@ -57,7 +60,7 @@ export function formatAuthChoiceChoicesForCli(params?: {
|
|||
}): string {
|
||||
const values = [
|
||||
...formatStaticAuthChoiceChoicesForCli(params).split("|"),
|
||||
...resolveManifestProviderSetupFlowContributions({
|
||||
...resolveProviderSetupFlowContributions({
|
||||
...params,
|
||||
scope: "text-inference",
|
||||
}).map((contribution) => contribution.option.value),
|
||||
|
|
|
|||
|
|
@ -48,8 +48,7 @@ function resolveProviderChoiceModelAllowlist(params: {
|
|||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
});
|
||||
return resolveProviderPluginChoice({
|
||||
providers,
|
||||
|
|
|
|||
|
|
@ -31,8 +31,7 @@ export async function maybeRepairLegacyOAuthProfileIds(
|
|||
const providers = resolvePluginProviders({
|
||||
config: cfg,
|
||||
env: process.env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
});
|
||||
for (const provider of providers) {
|
||||
for (const repairSpec of provider.oauthProfileIdRepairs ?? []) {
|
||||
|
|
@ -134,8 +133,7 @@ export async function maybeRemoveDeprecatedCliAuthProfiles(
|
|||
const providers = resolvePluginProviders({
|
||||
config: cfg,
|
||||
env: process.env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
});
|
||||
const deprecatedEntries = providers.flatMap((provider) =>
|
||||
(provider.deprecatedProfileIds ?? [])
|
||||
|
|
|
|||
|
|
@ -189,8 +189,7 @@ describe("promptDefaultModel", () => {
|
|||
config,
|
||||
workspaceDir: undefined,
|
||||
env: undefined,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
});
|
||||
expect(result.model).toBe("vllm/meta-llama/Meta-Llama-3-8B-Instruct");
|
||||
expect(result.config?.models?.providers?.vllm).toMatchObject({
|
||||
|
|
|
|||
|
|
@ -110,8 +110,7 @@ async function resolveModelsAuthContext(): Promise<ResolvedModelsAuthContext> {
|
|||
const providers = resolvePluginProviders({
|
||||
config,
|
||||
workspaceDir,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
});
|
||||
return { config, agentDir, workspaceDir, providers };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,8 +110,7 @@ export async function applyNonInteractivePluginProviderChoice(params: {
|
|||
config: resolutionConfig,
|
||||
workspaceDir,
|
||||
onlyPluginIds: owningPluginIds,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
}),
|
||||
choice: params.authChoice,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,27 +6,19 @@ const mocks = vi.hoisted(() => ({
|
|||
resolvePluginWebSearchProviders: vi.fn<
|
||||
(params?: { config?: OpenClawConfig }) => PluginWebSearchProviderEntry[]
|
||||
>(() => []),
|
||||
listBundledWebSearchProviders: vi.fn<() => PluginWebSearchProviderEntry[]>(() => []),
|
||||
resolveBundledWebSearchPluginId: vi.fn<(providerId?: string) => string | undefined>(
|
||||
() => undefined,
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/web-search-providers.runtime.js", () => ({
|
||||
resolvePluginWebSearchProviders: mocks.resolvePluginWebSearchProviders,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/bundled-web-search.js", () => ({
|
||||
listBundledWebSearchProviders: mocks.listBundledWebSearchProviders,
|
||||
resolveBundledWebSearchPluginId: mocks.resolveBundledWebSearchPluginId,
|
||||
}));
|
||||
|
||||
function createCustomProviderEntry(): PluginWebSearchProviderEntry {
|
||||
return {
|
||||
id: "custom-search" as never,
|
||||
pluginId: "custom-plugin",
|
||||
label: "Custom Search",
|
||||
hint: "Custom provider",
|
||||
onboardingScopes: ["text-inference"],
|
||||
envVars: ["CUSTOM_SEARCH_API_KEY"],
|
||||
placeholder: "custom-...",
|
||||
signupUrl: "https://example.com/custom",
|
||||
|
|
@ -83,6 +75,7 @@ function createBundledDuckDuckGoEntry(): PluginWebSearchProviderEntry {
|
|||
pluginId: "duckduckgo",
|
||||
label: "DuckDuckGo Search (experimental)",
|
||||
hint: "Free fallback",
|
||||
onboardingScopes: ["text-inference"],
|
||||
requiresCredential: false,
|
||||
envVars: [],
|
||||
placeholder: "(no key needed)",
|
||||
|
|
@ -196,10 +189,7 @@ describe("onboard-search provider resolution", () => {
|
|||
});
|
||||
|
||||
it("does not treat hard-disabled bundled providers as selectable credentials", async () => {
|
||||
const firecrawlEntry = createBundledFirecrawlEntry();
|
||||
mocks.resolvePluginWebSearchProviders.mockReturnValue([]);
|
||||
mocks.listBundledWebSearchProviders.mockReturnValue([firecrawlEntry]);
|
||||
mocks.resolveBundledWebSearchPluginId.mockReturnValue("firecrawl");
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
tools: {
|
||||
|
|
@ -257,7 +247,7 @@ describe("onboard-search provider resolution", () => {
|
|||
expect(notes.some((message) => message.includes("works without an API key"))).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps the legacy default onboarding search surface when no config is present", async () => {
|
||||
it("uses the runtime onboarding search surface when no config is present", async () => {
|
||||
const firecrawlEntry = createBundledFirecrawlEntry();
|
||||
const duckduckgoEntry = createBundledDuckDuckGoEntry();
|
||||
const tavilyEntry: PluginWebSearchProviderEntry = {
|
||||
|
|
@ -272,16 +262,20 @@ describe("onboard-search provider resolution", () => {
|
|||
};
|
||||
const customEntry = createCustomProviderEntry();
|
||||
|
||||
mocks.listBundledWebSearchProviders.mockReturnValue([
|
||||
mocks.resolvePluginWebSearchProviders.mockReturnValue([
|
||||
customEntry,
|
||||
duckduckgoEntry,
|
||||
firecrawlEntry,
|
||||
tavilyEntry,
|
||||
]);
|
||||
mocks.resolvePluginWebSearchProviders.mockReturnValue([customEntry]);
|
||||
|
||||
const options = mod.resolveSearchProviderOptions();
|
||||
|
||||
expect(options.map((entry) => entry.id)).toEqual(["firecrawl", "tavily"]);
|
||||
expect(options.map((entry) => entry.id)).toEqual([
|
||||
"custom-search",
|
||||
"duckduckgo",
|
||||
"firecrawl",
|
||||
"tavily",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import { SEARCH_PROVIDER_OPTIONS, setupSearch } from "./onboard-search.js";
|
||||
import { listSearchProviderOptions, setupSearch } from "./onboard-search.js";
|
||||
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
|
|
@ -614,8 +614,9 @@ describe("setupSearch", () => {
|
|||
});
|
||||
|
||||
it("exports all 7 providers in alphabetical order", () => {
|
||||
const values = SEARCH_PROVIDER_OPTIONS.map((e) => e.id);
|
||||
expect(SEARCH_PROVIDER_OPTIONS).toHaveLength(7);
|
||||
const providers = listSearchProviderOptions();
|
||||
const values = providers.map((e) => e.id);
|
||||
expect(providers).toHaveLength(7);
|
||||
expect(values).toEqual([
|
||||
"brave",
|
||||
"firecrawl",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
export {
|
||||
SEARCH_PROVIDER_OPTIONS,
|
||||
applySearchKey,
|
||||
applySearchProviderSelection,
|
||||
hasExistingKey,
|
||||
hasKeyInEnv,
|
||||
listSearchProviderOptions,
|
||||
resolveExistingKey,
|
||||
resolveSearchProviderOptions,
|
||||
runSearchSetupFlow as setupSearch,
|
||||
|
|
|
|||
|
|
@ -27,8 +27,7 @@ export function resolveProviderAuthLoginCommand(params: {
|
|||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
}).find((candidate) => matchesProviderId(candidate, params.provider));
|
||||
if (!provider || provider.auth.length === 0) {
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -288,8 +288,7 @@ async function maybeHandleProviderPluginSelection(params: {
|
|||
config: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
});
|
||||
pluginResolution = pluginProviders.some(
|
||||
(provider) => normalizeProviderId(provider.id) === normalizeProviderId(params.selection),
|
||||
|
|
@ -318,8 +317,7 @@ async function maybeHandleProviderPluginSelection(params: {
|
|||
config: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
});
|
||||
}
|
||||
const resolved = resolveProviderPluginChoice({
|
||||
|
|
|
|||
|
|
@ -1,18 +1,13 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
resolveManifestProviderSetupFlowContributions,
|
||||
resolveProviderSetupFlowContributions,
|
||||
resolveProviderModelPickerFlowContributions,
|
||||
} from "./provider-flow.js";
|
||||
|
||||
const resolveManifestProviderAuthChoices = vi.hoisted(() => vi.fn(() => []));
|
||||
const resolveProviderWizardOptions = vi.hoisted(() => vi.fn(() => []));
|
||||
const resolveProviderModelPickerEntries = vi.hoisted(() => vi.fn(() => []));
|
||||
const resolvePluginProviders = vi.hoisted(() => vi.fn(() => []));
|
||||
|
||||
vi.mock("../plugins/provider-auth-choices.js", () => ({
|
||||
resolveManifestProviderAuthChoices,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/provider-wizard.js", () => ({
|
||||
resolveProviderWizardOptions,
|
||||
resolveProviderModelPickerEntries,
|
||||
|
|
@ -27,21 +22,20 @@ describe("provider flow", () => {
|
|||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("uses bundled compat when resolving docs for manifest-backed setup contributions", () => {
|
||||
resolveManifestProviderAuthChoices.mockReturnValue([
|
||||
it("uses setup mode when resolving docs for setup contributions", () => {
|
||||
resolveProviderWizardOptions.mockReturnValue([
|
||||
{
|
||||
pluginId: "sglang",
|
||||
providerId: "sglang",
|
||||
methodId: "custom",
|
||||
choiceId: "provider-plugin:sglang:custom",
|
||||
choiceLabel: "SGLang",
|
||||
value: "provider-plugin:sglang:custom",
|
||||
label: "SGLang",
|
||||
groupId: "sglang",
|
||||
groupLabel: "SGLang",
|
||||
},
|
||||
] as never);
|
||||
resolvePluginProviders.mockReturnValue([
|
||||
{ id: "sglang", docsPath: "/providers/sglang" },
|
||||
] as never);
|
||||
|
||||
const contributions = resolveManifestProviderSetupFlowContributions({
|
||||
const contributions = resolveProviderSetupFlowContributions({
|
||||
config: {},
|
||||
workspaceDir: "/tmp/workspace",
|
||||
env: process.env,
|
||||
|
|
@ -51,13 +45,13 @@ describe("provider flow", () => {
|
|||
config: {},
|
||||
workspaceDir: "/tmp/workspace",
|
||||
env: process.env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
});
|
||||
expect(contributions[0]?.option.docs).toEqual({ path: "/providers/sglang" });
|
||||
expect(contributions[0]?.source).toBe("runtime");
|
||||
});
|
||||
|
||||
it("uses bundled compat when resolving docs for runtime model-picker contributions", () => {
|
||||
it("uses setup mode when resolving docs for runtime model-picker contributions", () => {
|
||||
resolveProviderModelPickerEntries.mockReturnValue([
|
||||
{
|
||||
value: "provider-plugin:vllm:custom",
|
||||
|
|
@ -76,8 +70,7 @@ describe("provider flow", () => {
|
|||
config: {},
|
||||
workspaceDir: "/tmp/workspace",
|
||||
env: process.env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
});
|
||||
expect(contributions[0]?.option.docs).toEqual({ path: "/providers/vllm" });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveManifestProviderAuthChoices } from "../plugins/provider-auth-choices.js";
|
||||
import {
|
||||
resolveProviderModelPickerEntries,
|
||||
resolveProviderWizardOptions,
|
||||
|
|
@ -7,7 +6,7 @@ import {
|
|||
import { resolvePluginProviders } from "../plugins/providers.runtime.js";
|
||||
import type { ProviderPlugin } from "../plugins/types.js";
|
||||
import type { FlowContribution, FlowOption } from "./types.js";
|
||||
import { mergeFlowContributions, sortFlowContributionsByLabel } from "./types.js";
|
||||
import { sortFlowContributionsByLabel } from "./types.js";
|
||||
|
||||
export type ProviderFlowScope = "text-inference" | "image-generation";
|
||||
|
||||
|
|
@ -26,7 +25,7 @@ export type ProviderSetupFlowContribution = FlowContribution & {
|
|||
pluginId?: string;
|
||||
option: ProviderSetupFlowOption;
|
||||
onboardingScopes?: ProviderFlowScope[];
|
||||
source: "manifest" | "runtime";
|
||||
source: "runtime";
|
||||
};
|
||||
|
||||
export type ProviderModelPickerFlowContribution = FlowContribution & {
|
||||
|
|
@ -54,8 +53,7 @@ function resolveProviderDocsById(params?: {
|
|||
config: params?.config,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
env: params?.env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
})
|
||||
.filter((provider): provider is ProviderPlugin & { docsPath: string } =>
|
||||
Boolean(provider.docsPath?.trim()),
|
||||
|
|
@ -64,103 +62,6 @@ function resolveProviderDocsById(params?: {
|
|||
);
|
||||
}
|
||||
|
||||
export function resolveManifestProviderSetupFlowOptions(params?: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
scope?: ProviderFlowScope;
|
||||
}): ProviderSetupFlowOption[] {
|
||||
return resolveManifestProviderSetupFlowContributions(params).map(
|
||||
(contribution) => contribution.option,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveManifestProviderSetupFlowContributions(params?: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
scope?: ProviderFlowScope;
|
||||
}): ProviderSetupFlowContribution[] {
|
||||
const scope = params?.scope ?? DEFAULT_PROVIDER_FLOW_SCOPE;
|
||||
const docsByProvider = resolveProviderDocsById(params ?? {});
|
||||
return resolveManifestProviderAuthChoices(params)
|
||||
.filter((choice) => includesProviderFlowScope(choice.onboardingScopes, scope))
|
||||
.map((choice) => ({
|
||||
id: `provider:setup:${choice.choiceId}`,
|
||||
kind: "provider" as const,
|
||||
surface: "setup" as const,
|
||||
providerId: choice.providerId,
|
||||
pluginId: choice.pluginId,
|
||||
option: {
|
||||
value: choice.choiceId,
|
||||
label: choice.choiceLabel,
|
||||
...(choice.choiceHint ? { hint: choice.choiceHint } : {}),
|
||||
...(choice.assistantPriority !== undefined
|
||||
? { assistantPriority: choice.assistantPriority }
|
||||
: {}),
|
||||
...(choice.assistantVisibility ? { assistantVisibility: choice.assistantVisibility } : {}),
|
||||
...(choice.groupId && choice.groupLabel
|
||||
? {
|
||||
group: {
|
||||
id: choice.groupId,
|
||||
label: choice.groupLabel,
|
||||
...(choice.groupHint ? { hint: choice.groupHint } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(docsByProvider.get(choice.providerId)
|
||||
? { docs: { path: docsByProvider.get(choice.providerId)! } }
|
||||
: {}),
|
||||
},
|
||||
...(choice.onboardingScopes ? { onboardingScopes: [...choice.onboardingScopes] } : {}),
|
||||
source: "manifest" as const,
|
||||
}));
|
||||
}
|
||||
|
||||
export function resolveRuntimeFallbackProviderSetupFlowOptions(params?: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
scope?: ProviderFlowScope;
|
||||
}): ProviderSetupFlowOption[] {
|
||||
return resolveRuntimeFallbackProviderSetupFlowContributions(params).map(
|
||||
(contribution) => contribution.option,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveRuntimeFallbackProviderSetupFlowContributions(params?: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
scope?: ProviderFlowScope;
|
||||
}): ProviderSetupFlowContribution[] {
|
||||
const scope = params?.scope ?? DEFAULT_PROVIDER_FLOW_SCOPE;
|
||||
return resolveProviderWizardOptions(params ?? {})
|
||||
.filter((option) => includesProviderFlowScope(option.onboardingScopes, scope))
|
||||
.map((option) => ({
|
||||
id: `provider:setup:${option.value}`,
|
||||
kind: "provider" as const,
|
||||
surface: "setup" as const,
|
||||
providerId: option.groupId,
|
||||
option: {
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
...(option.hint ? { hint: option.hint } : {}),
|
||||
...(option.assistantPriority !== undefined
|
||||
? { assistantPriority: option.assistantPriority }
|
||||
: {}),
|
||||
...(option.assistantVisibility ? { assistantVisibility: option.assistantVisibility } : {}),
|
||||
group: {
|
||||
id: option.groupId,
|
||||
label: option.groupLabel,
|
||||
...(option.groupHint ? { hint: option.groupHint } : {}),
|
||||
},
|
||||
},
|
||||
...(option.onboardingScopes ? { onboardingScopes: [...option.onboardingScopes] } : {}),
|
||||
source: "runtime" as const,
|
||||
}));
|
||||
}
|
||||
|
||||
export function resolveProviderSetupFlowOptions(params?: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
|
|
@ -176,11 +77,38 @@ export function resolveProviderSetupFlowContributions(params?: {
|
|||
env?: NodeJS.ProcessEnv;
|
||||
scope?: ProviderFlowScope;
|
||||
}): ProviderSetupFlowContribution[] {
|
||||
const scope = params?.scope ?? DEFAULT_PROVIDER_FLOW_SCOPE;
|
||||
const docsByProvider = resolveProviderDocsById(params ?? {});
|
||||
return sortFlowContributionsByLabel(
|
||||
mergeFlowContributions({
|
||||
primary: resolveManifestProviderSetupFlowContributions(params),
|
||||
fallbacks: resolveRuntimeFallbackProviderSetupFlowContributions(params),
|
||||
}),
|
||||
resolveProviderWizardOptions(params ?? {})
|
||||
.filter((option) => includesProviderFlowScope(option.onboardingScopes, scope))
|
||||
.map((option) => ({
|
||||
id: `provider:setup:${option.value}`,
|
||||
kind: "provider" as const,
|
||||
surface: "setup" as const,
|
||||
providerId: option.groupId,
|
||||
option: {
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
...(option.hint ? { hint: option.hint } : {}),
|
||||
...(option.assistantPriority !== undefined
|
||||
? { assistantPriority: option.assistantPriority }
|
||||
: {}),
|
||||
...(option.assistantVisibility
|
||||
? { assistantVisibility: option.assistantVisibility }
|
||||
: {}),
|
||||
group: {
|
||||
id: option.groupId,
|
||||
label: option.groupLabel,
|
||||
...(option.groupHint ? { hint: option.groupHint } : {}),
|
||||
},
|
||||
...(docsByProvider.get(option.groupId)
|
||||
? { docs: { path: docsByProvider.get(option.groupId)! } }
|
||||
: {}),
|
||||
},
|
||||
...(option.onboardingScopes ? { onboardingScopes: [...option.onboardingScopes] } : {}),
|
||||
source: "runtime" as const,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -92,12 +92,6 @@ const mockGrokProvider = vi.hoisted(() => ({
|
|||
},
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/bundled-web-search.js", () => ({
|
||||
listBundledWebSearchProviders: () => [mockGrokProvider],
|
||||
resolveBundledWebSearchPluginId: (providerId: string | undefined) =>
|
||||
providerId === "grok" ? "xai" : undefined,
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/web-search-providers.runtime.js", () => ({
|
||||
resolvePluginWebSearchProviders: () => [mockGrokProvider],
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -7,10 +7,6 @@ import {
|
|||
hasConfiguredSecretInput,
|
||||
normalizeSecretInputString,
|
||||
} from "../config/types.secrets.js";
|
||||
import {
|
||||
listBundledWebSearchProviders,
|
||||
resolveBundledWebSearchPluginId,
|
||||
} from "../plugins/bundled-web-search.js";
|
||||
import { enablePluginInConfig } from "../plugins/enable.js";
|
||||
import type { PluginWebSearchProviderEntry } from "../plugins/types.js";
|
||||
import { resolvePluginWebSearchProviders } from "../plugins/web-search-providers.runtime.js";
|
||||
|
|
@ -35,7 +31,7 @@ export type SearchProviderSetupContribution = FlowContribution & {
|
|||
surface: "setup";
|
||||
provider: PluginWebSearchProviderEntry;
|
||||
option: SearchProviderSetupOption;
|
||||
source: "bundled" | "runtime";
|
||||
source: "runtime";
|
||||
};
|
||||
|
||||
function resolveSearchProviderCredentialLabel(
|
||||
|
|
@ -47,8 +43,11 @@ function resolveSearchProviderCredentialLabel(
|
|||
return entry.credentialLabel?.trim() || `${entry.label} API key`;
|
||||
}
|
||||
|
||||
export const SEARCH_PROVIDER_OPTIONS: readonly PluginWebSearchProviderEntry[] =
|
||||
resolveSearchProviderSetupContributions().map((contribution) => contribution.provider);
|
||||
export function listSearchProviderOptions(
|
||||
config?: OpenClawConfig,
|
||||
): readonly PluginWebSearchProviderEntry[] {
|
||||
return resolveSearchProviderOptions(config);
|
||||
}
|
||||
|
||||
function showsSearchProviderInSetup(
|
||||
entry: Pick<PluginWebSearchProviderEntry, "onboardingScopes">,
|
||||
|
|
@ -56,20 +55,6 @@ function showsSearchProviderInSetup(
|
|||
return entry.onboardingScopes?.includes("text-inference") ?? false;
|
||||
}
|
||||
|
||||
function canRepairBundledProviderSelection(
|
||||
config: OpenClawConfig,
|
||||
provider: Pick<PluginWebSearchProviderEntry, "id" | "pluginId">,
|
||||
): boolean {
|
||||
const pluginId = provider.pluginId ?? resolveBundledWebSearchPluginId(provider.id);
|
||||
if (!pluginId) {
|
||||
return false;
|
||||
}
|
||||
if (config.plugins?.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
return !config.plugins?.deny?.includes(pluginId);
|
||||
}
|
||||
|
||||
export function resolveSearchProviderOptions(
|
||||
config?: OpenClawConfig,
|
||||
): readonly PluginWebSearchProviderEntry[] {
|
||||
|
|
@ -80,7 +65,7 @@ export function resolveSearchProviderOptions(
|
|||
|
||||
function buildSearchProviderSetupContribution(params: {
|
||||
provider: PluginWebSearchProviderEntry;
|
||||
source: "bundled" | "runtime";
|
||||
source: "runtime";
|
||||
}): SearchProviderSetupContribution {
|
||||
return {
|
||||
id: `search:setup:${params.provider.id}`,
|
||||
|
|
@ -100,33 +85,18 @@ function buildSearchProviderSetupContribution(params: {
|
|||
export function resolveSearchProviderSetupContributions(
|
||||
config?: OpenClawConfig,
|
||||
): SearchProviderSetupContribution[] {
|
||||
if (!config) {
|
||||
return sortFlowContributionsByLabel(
|
||||
sortWebSearchProviders(listBundledWebSearchProviders())
|
||||
.filter(showsSearchProviderInSetup)
|
||||
.map((provider) => buildSearchProviderSetupContribution({ provider, source: "bundled" })),
|
||||
);
|
||||
}
|
||||
|
||||
const merged = new Map<string, SearchProviderSetupContribution>(
|
||||
const providers = sortWebSearchProviders(
|
||||
resolvePluginWebSearchProviders({
|
||||
config,
|
||||
bundledAllowlistCompat: true,
|
||||
env: process.env,
|
||||
}).map((provider) => [
|
||||
provider.id,
|
||||
buildSearchProviderSetupContribution({ provider, source: "runtime" }),
|
||||
]),
|
||||
mode: "setup",
|
||||
}),
|
||||
);
|
||||
return sortFlowContributionsByLabel(
|
||||
providers
|
||||
.filter(showsSearchProviderInSetup)
|
||||
.map((provider) => buildSearchProviderSetupContribution({ provider, source: "runtime" })),
|
||||
);
|
||||
|
||||
for (const provider of listBundledWebSearchProviders()) {
|
||||
if (merged.has(provider.id) || !canRepairBundledProviderSelection(config, provider)) {
|
||||
continue;
|
||||
}
|
||||
merged.set(provider.id, buildSearchProviderSetupContribution({ provider, source: "bundled" }));
|
||||
}
|
||||
|
||||
return sortFlowContributionsByLabel([...merged.values()]);
|
||||
}
|
||||
|
||||
function resolveSearchProviderEntry(
|
||||
|
|
@ -182,8 +152,8 @@ export function hasExistingKey(config: OpenClawConfig, provider: SearchProvider)
|
|||
function buildSearchEnvRef(config: OpenClawConfig, provider: SearchProvider): SecretRef {
|
||||
const entry =
|
||||
resolveSearchProviderEntry(config, provider) ??
|
||||
SEARCH_PROVIDER_OPTIONS.find((candidate) => candidate.id === provider) ??
|
||||
listBundledWebSearchProviders().find((candidate) => candidate.id === provider);
|
||||
listSearchProviderOptions(config).find((candidate) => candidate.id === provider) ??
|
||||
listSearchProviderOptions().find((candidate) => candidate.id === provider);
|
||||
const envVar = entry?.envVars.find((k) => Boolean(process.env[k]?.trim())) ?? entry?.envVars[0];
|
||||
if (!envVar) {
|
||||
throw new Error(
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ describe("agent event handler", () => {
|
|||
const FALLBACK_LIFECYCLE_DATA = {
|
||||
phase: "fallback",
|
||||
selectedProvider: "fireworks",
|
||||
selectedModel: "fireworks/minimax-m2p5",
|
||||
selectedModel: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
activeProvider: "deepinfra",
|
||||
activeModel: "moonshotai/Kimi-K2.5",
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -47,6 +47,38 @@ export type BundledPluginCompatibleActivationInputs = PluginActivationInputs & {
|
|||
compatPluginIds: string[];
|
||||
};
|
||||
|
||||
export function withActivatedPluginIds(params: {
|
||||
config?: OpenClawConfig;
|
||||
pluginIds: readonly string[];
|
||||
}): OpenClawConfig | undefined {
|
||||
if (params.pluginIds.length === 0) {
|
||||
return params.config;
|
||||
}
|
||||
const allow = new Set(params.config?.plugins?.allow ?? []);
|
||||
const entries = {
|
||||
...params.config?.plugins?.entries,
|
||||
};
|
||||
for (const pluginId of params.pluginIds) {
|
||||
const normalized = pluginId.trim();
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
allow.add(normalized);
|
||||
entries[normalized] = {
|
||||
...entries[normalized],
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...params.config,
|
||||
plugins: {
|
||||
...params.config?.plugins,
|
||||
...(allow.size > 0 ? { allow: [...allow] } : {}),
|
||||
entries,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyPluginCompatibilityOverrides(params: {
|
||||
config?: OpenClawConfig;
|
||||
compat?: PluginActivationCompatConfig;
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
// Auto-generated by scripts/generate-bundled-provider-auth-env-vars.mjs. Do not edit directly.
|
||||
|
||||
export const BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES = {
|
||||
anthropic: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
|
||||
brave: ["BRAVE_API_KEY"],
|
||||
byteplus: ["BYTEPLUS_API_KEY"],
|
||||
chutes: ["CHUTES_API_KEY", "CHUTES_OAUTH_TOKEN"],
|
||||
"cloudflare-ai-gateway": ["CLOUDFLARE_AI_GATEWAY_API_KEY"],
|
||||
deepgram: ["DEEPGRAM_API_KEY"],
|
||||
deepseek: ["DEEPSEEK_API_KEY"],
|
||||
exa: ["EXA_API_KEY"],
|
||||
fal: ["FAL_KEY"],
|
||||
firecrawl: ["FIRECRAWL_API_KEY"],
|
||||
"github-copilot": ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"],
|
||||
google: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
||||
groq: ["GROQ_API_KEY"],
|
||||
huggingface: ["HUGGINGFACE_HUB_TOKEN", "HF_TOKEN"],
|
||||
kilocode: ["KILOCODE_API_KEY"],
|
||||
kimi: ["KIMI_API_KEY", "KIMICODE_API_KEY"],
|
||||
"kimi-coding": ["KIMI_API_KEY", "KIMICODE_API_KEY"],
|
||||
litellm: ["LITELLM_API_KEY"],
|
||||
"microsoft-foundry": ["AZURE_OPENAI_API_KEY"],
|
||||
minimax: ["MINIMAX_API_KEY"],
|
||||
"minimax-portal": ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"],
|
||||
mistral: ["MISTRAL_API_KEY"],
|
||||
moonshot: ["MOONSHOT_API_KEY"],
|
||||
nvidia: ["NVIDIA_API_KEY"],
|
||||
ollama: ["OLLAMA_API_KEY"],
|
||||
openai: ["OPENAI_API_KEY"],
|
||||
opencode: ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"],
|
||||
"opencode-go": ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"],
|
||||
openrouter: ["OPENROUTER_API_KEY"],
|
||||
perplexity: ["PERPLEXITY_API_KEY", "OPENROUTER_API_KEY"],
|
||||
qianfan: ["QIANFAN_API_KEY"],
|
||||
qwen: ["QWEN_API_KEY", "MODELSTUDIO_API_KEY", "DASHSCOPE_API_KEY"],
|
||||
sglang: ["SGLANG_API_KEY"],
|
||||
stepfun: ["STEPFUN_API_KEY"],
|
||||
"stepfun-plan": ["STEPFUN_API_KEY"],
|
||||
synthetic: ["SYNTHETIC_API_KEY"],
|
||||
tavily: ["TAVILY_API_KEY"],
|
||||
together: ["TOGETHER_API_KEY"],
|
||||
venice: ["VENICE_API_KEY"],
|
||||
"vercel-ai-gateway": ["AI_GATEWAY_API_KEY"],
|
||||
vllm: ["VLLM_API_KEY"],
|
||||
volcengine: ["VOLCANO_ENGINE_API_KEY"],
|
||||
xai: ["XAI_API_KEY"],
|
||||
xiaomi: ["XIAOMI_API_KEY"],
|
||||
zai: ["ZAI_API_KEY", "Z_AI_API_KEY"],
|
||||
} as const satisfies Record<string, readonly string[]>;
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
collectBundledProviderAuthEnvVars,
|
||||
writeBundledProviderAuthEnvVarModule,
|
||||
} from "../../scripts/generate-bundled-provider-auth-env-vars.mjs";
|
||||
import { BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES } from "./bundled-provider-auth-env-vars.js";
|
||||
import {
|
||||
createGeneratedPluginTempRoot,
|
||||
installGeneratedPluginTempRootCleanup,
|
||||
pluginTestRepoRoot as repoRoot,
|
||||
writeJson,
|
||||
} from "./generated-plugin-test-helpers.js";
|
||||
|
||||
installGeneratedPluginTempRootCleanup();
|
||||
|
||||
function expectGeneratedAuthEnvVarModuleState(params: {
|
||||
tempRoot: string;
|
||||
expectedChanged: boolean;
|
||||
expectedWrote: boolean;
|
||||
}) {
|
||||
const result = writeBundledProviderAuthEnvVarModule({
|
||||
repoRoot: params.tempRoot,
|
||||
outputPath: "src/plugins/bundled-provider-auth-env-vars.generated.ts",
|
||||
check: true,
|
||||
});
|
||||
expect(result.changed).toBe(params.expectedChanged);
|
||||
expect(result.wrote).toBe(params.expectedWrote);
|
||||
}
|
||||
|
||||
function expectGeneratedAuthEnvVarCheckMode(tempRoot: string) {
|
||||
expectGeneratedAuthEnvVarModuleState({
|
||||
tempRoot,
|
||||
expectedChanged: false,
|
||||
expectedWrote: false,
|
||||
});
|
||||
}
|
||||
|
||||
function expectBundledProviderEnvVars(expected: Record<string, readonly string[]>) {
|
||||
expect(
|
||||
Object.fromEntries(
|
||||
Object.keys(expected).map((providerId) => [
|
||||
providerId,
|
||||
BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES[
|
||||
providerId as keyof typeof BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES
|
||||
],
|
||||
]),
|
||||
),
|
||||
).toEqual(expected);
|
||||
}
|
||||
|
||||
function expectMissingBundledProviderEnvVars(providerIds: readonly string[]) {
|
||||
providerIds.forEach((providerId) => {
|
||||
expect(providerId in BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES).toBe(false);
|
||||
});
|
||||
}
|
||||
|
||||
describe("bundled provider auth env vars", () => {
|
||||
it("matches the generated manifest snapshot", () => {
|
||||
expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES).toEqual(
|
||||
collectBundledProviderAuthEnvVars({ repoRoot }),
|
||||
);
|
||||
});
|
||||
|
||||
it("reads bundled provider auth env vars from plugin manifests", () => {
|
||||
expectBundledProviderEnvVars({
|
||||
brave: ["BRAVE_API_KEY"],
|
||||
deepgram: ["DEEPGRAM_API_KEY"],
|
||||
firecrawl: ["FIRECRAWL_API_KEY"],
|
||||
"github-copilot": ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"],
|
||||
groq: ["GROQ_API_KEY"],
|
||||
perplexity: ["PERPLEXITY_API_KEY", "OPENROUTER_API_KEY"],
|
||||
tavily: ["TAVILY_API_KEY"],
|
||||
"minimax-portal": ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"],
|
||||
openai: ["OPENAI_API_KEY"],
|
||||
fal: ["FAL_KEY"],
|
||||
});
|
||||
expectMissingBundledProviderEnvVars(["openai-codex"]);
|
||||
});
|
||||
|
||||
it("supports check mode for stale generated artifacts", () => {
|
||||
const tempRoot = createGeneratedPluginTempRoot("openclaw-provider-auth-env-vars-");
|
||||
|
||||
writeJson(path.join(tempRoot, "extensions", "alpha", "openclaw.plugin.json"), {
|
||||
id: "alpha",
|
||||
providerAuthEnvVars: {
|
||||
alpha: ["ALPHA_TOKEN"],
|
||||
},
|
||||
});
|
||||
|
||||
const initial = writeBundledProviderAuthEnvVarModule({
|
||||
repoRoot: tempRoot,
|
||||
outputPath: "src/plugins/bundled-provider-auth-env-vars.generated.ts",
|
||||
});
|
||||
expect(initial.wrote).toBe(true);
|
||||
|
||||
expectGeneratedAuthEnvVarCheckMode(tempRoot);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(tempRoot, "src/plugins/bundled-provider-auth-env-vars.generated.ts"),
|
||||
"// stale\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expectGeneratedAuthEnvVarModuleState({
|
||||
tempRoot,
|
||||
expectedChanged: true,
|
||||
expectedWrote: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
// Generated from extension manifests so core secrets/auth code does not need
|
||||
// static imports into extension source trees.
|
||||
export { BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES } from "./bundled-provider-auth-env-vars.generated.js";
|
||||
|
|
@ -1,7 +1 @@
|
|||
import { BUNDLED_WEB_FETCH_PLUGIN_IDS as BUNDLED_WEB_FETCH_PLUGIN_IDS_FROM_METADATA } from "./bundled-capability-metadata.js";
|
||||
|
||||
export const BUNDLED_WEB_FETCH_PLUGIN_IDS = BUNDLED_WEB_FETCH_PLUGIN_IDS_FROM_METADATA;
|
||||
|
||||
export function listBundledWebFetchPluginIds(): string[] {
|
||||
return [...BUNDLED_WEB_FETCH_PLUGIN_IDS];
|
||||
}
|
||||
export { resolveBundledWebFetchPluginIds as listBundledWebFetchPluginIds } from "./bundled-web-fetch.js";
|
||||
|
|
|
|||
|
|
@ -1,18 +1 @@
|
|||
import { BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS } from "./bundled-capability-metadata.js";
|
||||
|
||||
const bundledWebFetchProviderPluginIds = Object.fromEntries(
|
||||
BUNDLED_PLUGIN_CONTRACT_SNAPSHOTS.flatMap((entry) =>
|
||||
entry.webFetchProviderIds.map((providerId) => [providerId, entry.pluginId] as const),
|
||||
).toSorted(([left], [right]) => left.localeCompare(right)),
|
||||
) as Readonly<Record<string, string>>;
|
||||
|
||||
export function resolveBundledWebFetchPluginId(providerId: string | undefined): string | undefined {
|
||||
if (!providerId) {
|
||||
return undefined;
|
||||
}
|
||||
const normalizedProviderId = providerId.trim().toLowerCase();
|
||||
if (!(normalizedProviderId in bundledWebFetchProviderPluginIds)) {
|
||||
return undefined;
|
||||
}
|
||||
return bundledWebFetchProviderPluginIds[normalizedProviderId];
|
||||
}
|
||||
export { resolveBundledWebFetchPluginId } from "./bundled-web-fetch.js";
|
||||
|
|
|
|||
|
|
@ -1,25 +1,50 @@
|
|||
import { loadBundledCapabilityRuntimeRegistry } from "./bundled-capability-runtime.js";
|
||||
import { BUNDLED_WEB_FETCH_PLUGIN_IDS } from "./bundled-web-fetch-ids.js";
|
||||
import { resolveBundledWebFetchPluginId as resolveBundledWebFetchPluginIdFromMap } from "./bundled-web-fetch-provider-ids.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import type { PluginWebFetchProviderEntry } from "./types.js";
|
||||
|
||||
type BundledWebFetchProviderEntry = PluginWebFetchProviderEntry & { pluginId: string };
|
||||
|
||||
let bundledWebFetchProvidersCache: BundledWebFetchProviderEntry[] | null = null;
|
||||
const bundledWebFetchProvidersCache = new Map<string, BundledWebFetchProviderEntry[]>();
|
||||
|
||||
function loadBundledWebFetchProviders(): BundledWebFetchProviderEntry[] {
|
||||
if (!bundledWebFetchProvidersCache) {
|
||||
bundledWebFetchProvidersCache = loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds: BUNDLED_WEB_FETCH_PLUGIN_IDS,
|
||||
pluginSdkResolution: "dist",
|
||||
}).webFetchProviders.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
...entry.provider,
|
||||
}));
|
||||
function resolveBundledWebFetchManifestPlugins(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}) {
|
||||
return loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
}).plugins.filter(
|
||||
(plugin) =>
|
||||
plugin.origin === "bundled" && (plugin.contracts?.webFetchProviders?.length ?? 0) > 0,
|
||||
);
|
||||
}
|
||||
|
||||
function loadBundledWebFetchProviders(params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): BundledWebFetchProviderEntry[] {
|
||||
const pluginIds = resolveBundledWebFetchPluginIds(params ?? {});
|
||||
const cacheKey = pluginIds.join("\u0000");
|
||||
const cached = bundledWebFetchProvidersCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
return bundledWebFetchProvidersCache;
|
||||
const providers =
|
||||
pluginIds.length === 0
|
||||
? []
|
||||
: loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds,
|
||||
pluginSdkResolution: "dist",
|
||||
}).webFetchProviders.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
...entry.provider,
|
||||
}));
|
||||
bundledWebFetchProvidersCache.set(cacheKey, providers);
|
||||
return providers;
|
||||
}
|
||||
|
||||
export function resolveBundledWebFetchPluginIds(params: {
|
||||
|
|
@ -27,23 +52,41 @@ export function resolveBundledWebFetchPluginIds(params: {
|
|||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
const bundledWebFetchPluginIdSet = new Set<string>(BUNDLED_WEB_FETCH_PLUGIN_IDS);
|
||||
return loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
})
|
||||
.plugins.filter(
|
||||
(plugin) => plugin.origin === "bundled" && bundledWebFetchPluginIdSet.has(plugin.id),
|
||||
)
|
||||
return resolveBundledWebFetchManifestPlugins(params)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function listBundledWebFetchProviders(): PluginWebFetchProviderEntry[] {
|
||||
return loadBundledWebFetchProviders();
|
||||
export function listBundledWebFetchProviders(params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): PluginWebFetchProviderEntry[] {
|
||||
return loadBundledWebFetchProviders(params);
|
||||
}
|
||||
|
||||
export function resolveBundledWebFetchPluginId(providerId: string | undefined): string | undefined {
|
||||
return resolveBundledWebFetchPluginIdFromMap(providerId);
|
||||
export function resolveBundledWebFetchPluginId(
|
||||
providerId: string | undefined,
|
||||
params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
},
|
||||
): string | undefined {
|
||||
if (!providerId) {
|
||||
return undefined;
|
||||
}
|
||||
const normalizedProviderId = providerId.trim().toLowerCase();
|
||||
if (!normalizedProviderId) {
|
||||
return undefined;
|
||||
}
|
||||
return resolveBundledWebFetchManifestPlugins({
|
||||
config: params?.config,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
env: params?.env,
|
||||
}).find((plugin) =>
|
||||
plugin.contracts?.webFetchProviders?.some(
|
||||
(candidate) => candidate.trim().toLowerCase() === normalizedProviderId,
|
||||
),
|
||||
)?.id;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { BUNDLED_WEB_SEARCH_PLUGIN_IDS as BUNDLED_WEB_SEARCH_PLUGIN_IDS_FROM_METADATA } from "./bundled-capability-metadata.js";
|
||||
import { listBundledWebSearchPluginIds as listBundledWebSearchPluginIdsImpl } from "./bundled-web-search.js";
|
||||
|
||||
export const BUNDLED_WEB_SEARCH_PLUGIN_IDS = BUNDLED_WEB_SEARCH_PLUGIN_IDS_FROM_METADATA;
|
||||
export const BUNDLED_WEB_SEARCH_PLUGIN_IDS = listBundledWebSearchPluginIdsImpl();
|
||||
|
||||
export function listBundledWebSearchPluginIds(): string[] {
|
||||
return [...BUNDLED_WEB_SEARCH_PLUGIN_IDS];
|
||||
return listBundledWebSearchPluginIdsImpl();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1 @@
|
|||
import { BUNDLED_WEB_SEARCH_PROVIDER_PLUGIN_IDS } from "./bundled-capability-metadata.js";
|
||||
|
||||
export function resolveBundledWebSearchPluginId(
|
||||
providerId: string | undefined,
|
||||
): string | undefined {
|
||||
if (!providerId) {
|
||||
return undefined;
|
||||
}
|
||||
const normalizedProviderId = providerId.trim().toLowerCase();
|
||||
if (!(normalizedProviderId in BUNDLED_WEB_SEARCH_PROVIDER_PLUGIN_IDS)) {
|
||||
return undefined;
|
||||
}
|
||||
return BUNDLED_WEB_SEARCH_PROVIDER_PLUGIN_IDS[normalizedProviderId];
|
||||
}
|
||||
export { resolveBundledWebSearchPluginId } from "./bundled-web-search.js";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { loadBundledCapabilityRuntimeRegistry } from "./bundled-capability-runtime.js";
|
||||
import { BUNDLED_WEB_SEARCH_PLUGIN_IDS } from "./bundled-web-search-ids.js";
|
||||
import { hasBundledWebSearchCredential } from "./bundled-web-search-registry.js";
|
||||
import {
|
||||
listBundledWebSearchPluginIds,
|
||||
|
|
@ -58,8 +57,9 @@ describe("bundled web search helpers", () => {
|
|||
vi.clearAllMocks();
|
||||
vi.mocked(loadPluginManifestRegistry).mockReturnValue({
|
||||
plugins: [
|
||||
{ id: "xai", origin: "bundled" },
|
||||
{ id: "google", origin: "bundled" },
|
||||
{ id: "xai", origin: "bundled", contracts: { webSearchProviders: ["grok"] } },
|
||||
{ id: "google", origin: "bundled", contracts: { webSearchProviders: ["gemini"] } },
|
||||
{ id: "minimax", origin: "bundled", contracts: { webSearchProviders: ["minimax"] } },
|
||||
{ id: "noise", origin: "bundled" },
|
||||
{ id: "external-google", origin: "workspace" },
|
||||
] as never[],
|
||||
|
|
@ -92,7 +92,7 @@ describe("bundled web search helpers", () => {
|
|||
} as never);
|
||||
});
|
||||
|
||||
it("filters bundled manifest entries down to known bundled web search plugins", () => {
|
||||
it("returns bundled manifest-derived web search plugins from the registry", () => {
|
||||
expect(
|
||||
resolveBundledWebSearchPluginIds({
|
||||
config: {
|
||||
|
|
@ -103,7 +103,7 @@ describe("bundled web search helpers", () => {
|
|||
workspaceDir: "/tmp/workspace",
|
||||
env: { OPENCLAW_HOME: "/tmp/openclaw-home" },
|
||||
}),
|
||||
).toEqual(["google", "xai"]);
|
||||
).toEqual(["google", "minimax", "xai"]);
|
||||
expect(loadPluginManifestRegistry).toHaveBeenCalledWith({
|
||||
config: {
|
||||
plugins: {
|
||||
|
|
@ -117,8 +117,8 @@ describe("bundled web search helpers", () => {
|
|||
|
||||
it("returns a copy of the bundled plugin id fast-path list", () => {
|
||||
const listed = listBundledWebSearchPluginIds();
|
||||
expect(listed).toEqual([...BUNDLED_WEB_SEARCH_PLUGIN_IDS]);
|
||||
expect(listed).not.toBe(BUNDLED_WEB_SEARCH_PLUGIN_IDS);
|
||||
expect(listed).toEqual(["google", "minimax", "xai"]);
|
||||
expect(listed).not.toBe(listBundledWebSearchPluginIds());
|
||||
});
|
||||
|
||||
it("maps bundled provider ids back to their owning plugins", () => {
|
||||
|
|
@ -140,7 +140,7 @@ describe("bundled web search helpers", () => {
|
|||
]);
|
||||
expect(loadBundledCapabilityRuntimeRegistry).toHaveBeenCalledTimes(1);
|
||||
expect(loadBundledCapabilityRuntimeRegistry).toHaveBeenCalledWith({
|
||||
pluginIds: BUNDLED_WEB_SEARCH_PLUGIN_IDS,
|
||||
pluginIds: ["google", "minimax", "xai"],
|
||||
pluginSdkResolution: "dist",
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,25 +1,50 @@
|
|||
import { BUNDLED_WEB_SEARCH_PLUGIN_IDS } from "./bundled-capability-metadata.js";
|
||||
import { loadBundledCapabilityRuntimeRegistry } from "./bundled-capability-runtime.js";
|
||||
import { resolveBundledWebSearchPluginId as resolveBundledWebSearchPluginIdFromMap } from "./bundled-web-search-provider-ids.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import type { PluginWebSearchProviderEntry } from "./types.js";
|
||||
|
||||
type BundledWebSearchProviderEntry = PluginWebSearchProviderEntry & { pluginId: string };
|
||||
|
||||
let bundledWebSearchProvidersCache: BundledWebSearchProviderEntry[] | null = null;
|
||||
const bundledWebSearchProvidersCache = new Map<string, BundledWebSearchProviderEntry[]>();
|
||||
|
||||
function loadBundledWebSearchProviders(): BundledWebSearchProviderEntry[] {
|
||||
if (!bundledWebSearchProvidersCache) {
|
||||
bundledWebSearchProvidersCache = loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds: BUNDLED_WEB_SEARCH_PLUGIN_IDS,
|
||||
pluginSdkResolution: "dist",
|
||||
}).webSearchProviders.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
...entry.provider,
|
||||
}));
|
||||
function resolveBundledWebSearchManifestPlugins(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}) {
|
||||
return loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
}).plugins.filter(
|
||||
(plugin) =>
|
||||
plugin.origin === "bundled" && (plugin.contracts?.webSearchProviders?.length ?? 0) > 0,
|
||||
);
|
||||
}
|
||||
|
||||
function loadBundledWebSearchProviders(params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): BundledWebSearchProviderEntry[] {
|
||||
const pluginIds = resolveBundledWebSearchPluginIds(params ?? {});
|
||||
const cacheKey = pluginIds.join("\u0000");
|
||||
const cached = bundledWebSearchProvidersCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
return bundledWebSearchProvidersCache;
|
||||
const providers =
|
||||
pluginIds.length === 0
|
||||
? []
|
||||
: loadBundledCapabilityRuntimeRegistry({
|
||||
pluginIds,
|
||||
pluginSdkResolution: "dist",
|
||||
}).webSearchProviders.map((entry) => ({
|
||||
pluginId: entry.pluginId,
|
||||
...entry.provider,
|
||||
}));
|
||||
bundledWebSearchProvidersCache.set(cacheKey, providers);
|
||||
return providers;
|
||||
}
|
||||
|
||||
export function resolveBundledWebSearchPluginIds(params: {
|
||||
|
|
@ -27,29 +52,49 @@ export function resolveBundledWebSearchPluginIds(params: {
|
|||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
const bundledWebSearchPluginIdSet = new Set<string>(BUNDLED_WEB_SEARCH_PLUGIN_IDS);
|
||||
return loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
})
|
||||
.plugins.filter(
|
||||
(plugin) => plugin.origin === "bundled" && bundledWebSearchPluginIdSet.has(plugin.id),
|
||||
)
|
||||
return resolveBundledWebSearchManifestPlugins(params)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function listBundledWebSearchPluginIds(): string[] {
|
||||
return [...BUNDLED_WEB_SEARCH_PLUGIN_IDS];
|
||||
export function listBundledWebSearchPluginIds(params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
return resolveBundledWebSearchPluginIds(params ?? {});
|
||||
}
|
||||
|
||||
export function listBundledWebSearchProviders(): PluginWebSearchProviderEntry[] {
|
||||
return loadBundledWebSearchProviders();
|
||||
export function listBundledWebSearchProviders(params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): PluginWebSearchProviderEntry[] {
|
||||
return loadBundledWebSearchProviders(params);
|
||||
}
|
||||
|
||||
export function resolveBundledWebSearchPluginId(
|
||||
providerId: string | undefined,
|
||||
params?: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
},
|
||||
): string | undefined {
|
||||
return resolveBundledWebSearchPluginIdFromMap(providerId);
|
||||
if (!providerId) {
|
||||
return undefined;
|
||||
}
|
||||
const normalizedProviderId = providerId.trim().toLowerCase();
|
||||
if (!normalizedProviderId) {
|
||||
return undefined;
|
||||
}
|
||||
return resolveBundledWebSearchManifestPlugins({
|
||||
config: params?.config,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
env: params?.env,
|
||||
}).find((plugin) =>
|
||||
plugin.contracts?.webSearchProviders?.some(
|
||||
(candidate) => candidate.trim().toLowerCase() === normalizedProviderId,
|
||||
),
|
||||
)?.id;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,7 @@ export async function resolvePreferredProviderForAuthChoice(params: {
|
|||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
});
|
||||
const pluginResolved = resolveProviderPluginChoice({
|
||||
providers,
|
||||
|
|
|
|||
|
|
@ -174,8 +174,7 @@ export async function applyAuthChoiceLoadedPluginProvider(
|
|||
config: params.config,
|
||||
workspaceDir,
|
||||
env: params.env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
});
|
||||
const resolved = resolveProviderPluginChoice({
|
||||
providers,
|
||||
|
|
@ -256,8 +255,7 @@ export async function applyAuthChoicePluginProvider(
|
|||
config: nextConfig,
|
||||
workspaceDir,
|
||||
env: params.env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
});
|
||||
const provider = resolveProviderMatch(providers, options.providerId);
|
||||
if (!provider) {
|
||||
|
|
|
|||
|
|
@ -79,64 +79,6 @@ function createWizardRuntimeParams(params?: {
|
|||
};
|
||||
}
|
||||
|
||||
function expectWizardResolutionCount(params: {
|
||||
provider: ProviderPlugin;
|
||||
config?: object;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
expectedCount: number;
|
||||
}) {
|
||||
setResolvedProviders(params.provider);
|
||||
resolveProviderWizardOptions(
|
||||
createWizardRuntimeParams({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
}),
|
||||
);
|
||||
resolveProviderWizardOptions(
|
||||
createWizardRuntimeParams({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
}),
|
||||
);
|
||||
expectProviderResolutionCall({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
count: params.expectedCount,
|
||||
});
|
||||
}
|
||||
|
||||
function expectWizardCacheInvalidationCount(params: {
|
||||
provider: ProviderPlugin;
|
||||
config: { [key: string]: unknown };
|
||||
env: NodeJS.ProcessEnv;
|
||||
mutate: () => void;
|
||||
expectedCount?: number;
|
||||
}) {
|
||||
setResolvedProviders(params.provider);
|
||||
|
||||
resolveProviderWizardOptions(
|
||||
createWizardRuntimeParams({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
}),
|
||||
);
|
||||
|
||||
params.mutate();
|
||||
|
||||
resolveProviderWizardOptions(
|
||||
createWizardRuntimeParams({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
}),
|
||||
);
|
||||
|
||||
expectProviderResolutionCall({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
count: params.expectedCount ?? 2,
|
||||
});
|
||||
}
|
||||
|
||||
function expectProviderResolutionCall(params?: {
|
||||
config?: object;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
|
|
@ -146,8 +88,7 @@ function expectProviderResolutionCall(params?: {
|
|||
expect(resolvePluginProviders).toHaveBeenCalledTimes(params?.count ?? 1);
|
||||
expect(resolvePluginProviders).toHaveBeenCalledWith({
|
||||
...createWizardRuntimeParams(params),
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -351,7 +292,7 @@ describe("provider wizard boundaries", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it("reuses provider resolution across wizard consumers for the same config and env", () => {
|
||||
it("resolves providers in setup mode across wizard consumers", () => {
|
||||
const provider = createSglangWizardProvider({ includeModelPicker: true });
|
||||
const config = {};
|
||||
const env = createHomeEnv();
|
||||
|
|
@ -361,82 +302,9 @@ describe("provider wizard boundaries", () => {
|
|||
expect(resolveProviderWizardOptions(runtimeParams)).toHaveLength(1);
|
||||
expect(resolveProviderModelPickerEntries(runtimeParams)).toHaveLength(1);
|
||||
|
||||
expectProviderResolutionCall({ config, env });
|
||||
});
|
||||
|
||||
it("invalidates the wizard cache when config or env contents change in place", () => {
|
||||
const config = createSglangConfig();
|
||||
const env = createHomeEnv("-a");
|
||||
|
||||
expectWizardCacheInvalidationCount({
|
||||
provider: createSglangWizardProvider(),
|
||||
config,
|
||||
env,
|
||||
mutate: () => {
|
||||
config.plugins.allow = ["vllm"];
|
||||
env.OPENCLAW_HOME = "/tmp/openclaw-home-b";
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "skips provider-wizard memoization when plugin cache opt-outs are set",
|
||||
env: createHomeEnv("", {
|
||||
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "skips provider-wizard memoization when discovery cache ttl is zero",
|
||||
env: createHomeEnv("", {
|
||||
OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS: "0",
|
||||
}),
|
||||
},
|
||||
] as const)("$name", ({ env }) => {
|
||||
expectWizardResolutionCount({
|
||||
provider: createSglangWizardProvider(),
|
||||
config: createSglangConfig(),
|
||||
env,
|
||||
expectedCount: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("expires provider-wizard memoization after the shortest plugin cache ttl", () => {
|
||||
vi.useFakeTimers();
|
||||
const provider = createSglangWizardProvider();
|
||||
const config = {};
|
||||
const env = createHomeEnv("", {
|
||||
OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS: "5",
|
||||
OPENCLAW_PLUGIN_MANIFEST_CACHE_MS: "20",
|
||||
});
|
||||
setResolvedProviders(provider);
|
||||
const runtimeParams = createWizardRuntimeParams({ config, env });
|
||||
|
||||
resolveProviderWizardOptions(runtimeParams);
|
||||
vi.advanceTimersByTime(4);
|
||||
resolveProviderWizardOptions(runtimeParams);
|
||||
vi.advanceTimersByTime(2);
|
||||
resolveProviderWizardOptions(runtimeParams);
|
||||
|
||||
expectProviderResolutionCall({ config, env, count: 2 });
|
||||
});
|
||||
|
||||
it("invalidates provider-wizard snapshots when cache-control env values change in place", () => {
|
||||
const config = {};
|
||||
const env = createHomeEnv("", {
|
||||
OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS: "1000",
|
||||
});
|
||||
|
||||
expectWizardCacheInvalidationCount({
|
||||
provider: createSglangWizardProvider(),
|
||||
config,
|
||||
env,
|
||||
mutate: () => {
|
||||
env.OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS = "5";
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("routes model-selected hooks only to the matching provider", async () => {
|
||||
const matchingHook = vi.fn(async () => {});
|
||||
const otherHook = vi.fn(async () => {});
|
||||
|
|
|
|||
|
|
@ -2,11 +2,6 @@ import { DEFAULT_PROVIDER } from "../agents/defaults.js";
|
|||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import {
|
||||
buildPluginSnapshotCacheEnvKey,
|
||||
resolvePluginSnapshotCacheTtlMs,
|
||||
shouldUsePluginSnapshotCache,
|
||||
} from "./cache-controls.js";
|
||||
import { resolvePluginProviders } from "./providers.runtime.js";
|
||||
import type {
|
||||
ProviderAuthMethod,
|
||||
|
|
@ -16,26 +11,6 @@ import type {
|
|||
} from "./types.js";
|
||||
|
||||
export const PROVIDER_PLUGIN_CHOICE_PREFIX = "provider-plugin:";
|
||||
type ProviderWizardCacheEntry = {
|
||||
expiresAt: number;
|
||||
providers: ProviderPlugin[];
|
||||
};
|
||||
const providerWizardCache = new WeakMap<
|
||||
OpenClawConfig,
|
||||
WeakMap<NodeJS.ProcessEnv, Map<string, ProviderWizardCacheEntry>>
|
||||
>();
|
||||
|
||||
function buildProviderWizardCacheKey(params: {
|
||||
config: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): string {
|
||||
return JSON.stringify({
|
||||
workspaceDir: params.workspaceDir ?? "",
|
||||
config: params.config,
|
||||
env: buildPluginSnapshotCacheEnvKey(params.env),
|
||||
});
|
||||
}
|
||||
|
||||
export type ProviderWizardOption = {
|
||||
value: string;
|
||||
|
|
@ -135,53 +110,12 @@ function resolveProviderWizardProviders(params: {
|
|||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): ProviderPlugin[] {
|
||||
if (!params.config) {
|
||||
return resolvePluginProviders(params);
|
||||
}
|
||||
const env = params.env ?? process.env;
|
||||
if (!shouldUsePluginSnapshotCache(env)) {
|
||||
return resolvePluginProviders({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
});
|
||||
}
|
||||
const cacheKey = buildProviderWizardCacheKey({
|
||||
return resolvePluginProviders({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
env: params.env,
|
||||
mode: "setup",
|
||||
});
|
||||
const configCache = providerWizardCache.get(params.config);
|
||||
const envCache = configCache?.get(env);
|
||||
const cached = envCache?.get(cacheKey);
|
||||
if (cached && cached.expiresAt > Date.now()) {
|
||||
return cached.providers;
|
||||
}
|
||||
const providers = resolvePluginProviders({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
});
|
||||
const ttlMs = resolvePluginSnapshotCacheTtlMs(env);
|
||||
let nextConfigCache = configCache;
|
||||
if (!nextConfigCache) {
|
||||
nextConfigCache = new WeakMap<NodeJS.ProcessEnv, Map<string, ProviderWizardCacheEntry>>();
|
||||
providerWizardCache.set(params.config, nextConfigCache);
|
||||
}
|
||||
let nextEnvCache = nextConfigCache.get(env);
|
||||
if (!nextEnvCache) {
|
||||
nextEnvCache = new Map<string, ProviderWizardCacheEntry>();
|
||||
nextConfigCache.set(env, nextEnvCache);
|
||||
}
|
||||
nextEnvCache.set(cacheKey, {
|
||||
expiresAt: Date.now() + ttlMs,
|
||||
providers,
|
||||
});
|
||||
return providers;
|
||||
}
|
||||
|
||||
export function resolveProviderWizardOptions(params: {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { withActivatedPluginIds } from "./activation-context.js";
|
||||
import { resolveBundledPluginCompatibleActivationInputs } from "./activation-context.js";
|
||||
import { resolveRuntimePluginRegistry, type PluginLoadOptions } from "./loader.js";
|
||||
import {
|
||||
loadOpenClawPlugins,
|
||||
resolveRuntimePluginRegistry,
|
||||
type PluginLoadOptions,
|
||||
} from "./loader.js";
|
||||
import { createPluginLoaderLogger } from "./logger.js";
|
||||
import {
|
||||
resolveDiscoveredProviderPluginIds,
|
||||
resolveEnabledProviderPluginIds,
|
||||
resolveBundledProviderCompatPluginIds,
|
||||
resolveOwningPluginIdsForModelRefs,
|
||||
|
|
@ -12,38 +18,6 @@ import type { ProviderPlugin } from "./types.js";
|
|||
|
||||
const log = createSubsystemLogger("plugins");
|
||||
|
||||
function withRuntimeActivatedPluginIds(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
pluginIds: readonly string[];
|
||||
}): PluginLoadOptions["config"] {
|
||||
if (params.pluginIds.length === 0) {
|
||||
return params.config;
|
||||
}
|
||||
const allow = new Set(params.config?.plugins?.allow ?? []);
|
||||
const entries = {
|
||||
...params.config?.plugins?.entries,
|
||||
};
|
||||
for (const pluginId of params.pluginIds) {
|
||||
const normalized = pluginId.trim();
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
allow.add(normalized);
|
||||
entries[normalized] = {
|
||||
...entries[normalized],
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...params.config,
|
||||
plugins: {
|
||||
...params.config?.plugins,
|
||||
...(allow.size > 0 ? { allow: [...allow] } : {}),
|
||||
entries,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function resolvePluginProviders(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
|
|
@ -56,6 +30,7 @@ export function resolvePluginProviders(params: {
|
|||
activate?: boolean;
|
||||
cache?: boolean;
|
||||
pluginSdkResolution?: PluginLoadOptions["pluginSdkResolution"];
|
||||
mode?: "runtime" | "setup";
|
||||
}): ProviderPlugin[] {
|
||||
const env = params.env ?? process.env;
|
||||
const modelOwnedPluginIds = params.modelRefs?.length
|
||||
|
|
@ -70,10 +45,40 @@ export function resolvePluginProviders(params: {
|
|||
params.onlyPluginIds || modelOwnedPluginIds.length > 0
|
||||
? [...new Set([...(params.onlyPluginIds ?? []), ...modelOwnedPluginIds])]
|
||||
: undefined;
|
||||
const runtimeConfig = withRuntimeActivatedPluginIds({
|
||||
const runtimeConfig = withActivatedPluginIds({
|
||||
config: params.config,
|
||||
pluginIds: modelOwnedPluginIds,
|
||||
});
|
||||
if (params.mode === "setup") {
|
||||
const providerPluginIds = resolveDiscoveredProviderPluginIds({
|
||||
config: runtimeConfig,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: requestedPluginIds,
|
||||
});
|
||||
if (providerPluginIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const registry = loadOpenClawPlugins({
|
||||
config: withActivatedPluginIds({
|
||||
config: runtimeConfig,
|
||||
pluginIds: providerPluginIds,
|
||||
}),
|
||||
activationSourceConfig: runtimeConfig,
|
||||
autoEnabledReasons: {},
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: providerPluginIds,
|
||||
pluginSdkResolution: params.pluginSdkResolution,
|
||||
cache: params.cache ?? false,
|
||||
activate: params.activate ?? false,
|
||||
logger: createPluginLoaderLogger(log),
|
||||
});
|
||||
return registry.providers.map((entry) => ({
|
||||
...entry.provider,
|
||||
pluginId: entry.pluginId,
|
||||
}));
|
||||
}
|
||||
const activation = resolveBundledPluginCompatibleActivationInputs({
|
||||
rawConfig: runtimeConfig,
|
||||
env,
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ import { createEmptyPluginRegistry } from "./registry-empty.js";
|
|||
import type { ProviderPlugin } from "./types.js";
|
||||
|
||||
type ResolveRuntimePluginRegistry = typeof import("./loader.js").resolveRuntimePluginRegistry;
|
||||
type LoadOpenClawPlugins = typeof import("./loader.js").loadOpenClawPlugins;
|
||||
type LoadPluginManifestRegistry =
|
||||
typeof import("./manifest-registry.js").loadPluginManifestRegistry;
|
||||
type ApplyPluginAutoEnable = typeof import("../config/plugin-auto-enable.js").applyPluginAutoEnable;
|
||||
|
||||
const resolveRuntimePluginRegistryMock = vi.fn<ResolveRuntimePluginRegistry>();
|
||||
const loadOpenClawPluginsMock = vi.fn<LoadOpenClawPlugins>();
|
||||
const loadPluginManifestRegistryMock = vi.fn<LoadPluginManifestRegistry>();
|
||||
const applyPluginAutoEnableMock = vi.fn<ApplyPluginAutoEnable>();
|
||||
|
||||
|
|
@ -131,6 +133,20 @@ function expectLastRuntimeRegistryLoad(params?: {
|
|||
);
|
||||
}
|
||||
|
||||
function expectLastSetupRegistryLoad(params?: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
onlyPluginIds?: readonly string[];
|
||||
}) {
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cache: false,
|
||||
activate: false,
|
||||
...(params?.env ? { env: params.env } : {}),
|
||||
...(params?.onlyPluginIds ? { onlyPluginIds: params.onlyPluginIds } : {}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function getLastResolvedPluginConfig() {
|
||||
return getLastRuntimeRegistryCall().config as
|
||||
| {
|
||||
|
|
@ -142,6 +158,19 @@ function getLastResolvedPluginConfig() {
|
|||
| undefined;
|
||||
}
|
||||
|
||||
function getLastSetupLoadedPluginConfig() {
|
||||
const call = loadOpenClawPluginsMock.mock.calls.at(-1)?.[0];
|
||||
expect(call).toBeDefined();
|
||||
return (call?.config ?? undefined) as
|
||||
| {
|
||||
plugins?: {
|
||||
allow?: string[];
|
||||
entries?: Record<string, { enabled?: boolean }>;
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
|
||||
function createBundledProviderCompatOptions(params?: { onlyPluginIds?: readonly string[] }) {
|
||||
return {
|
||||
config: {
|
||||
|
|
@ -222,6 +251,8 @@ describe("resolvePluginProviders", () => {
|
|||
beforeAll(async () => {
|
||||
vi.resetModules();
|
||||
vi.doMock("./loader.js", () => ({
|
||||
loadOpenClawPlugins: (...args: Parameters<LoadOpenClawPlugins>) =>
|
||||
loadOpenClawPluginsMock(...args),
|
||||
resolveRuntimePluginRegistry: (...args: Parameters<ResolveRuntimePluginRegistry>) =>
|
||||
resolveRuntimePluginRegistryMock(...args),
|
||||
}));
|
||||
|
|
@ -243,6 +274,7 @@ describe("resolvePluginProviders", () => {
|
|||
|
||||
beforeEach(() => {
|
||||
resolveRuntimePluginRegistryMock.mockReset();
|
||||
loadOpenClawPluginsMock.mockReset();
|
||||
const provider: ProviderPlugin = {
|
||||
id: "demo-provider",
|
||||
label: "Demo Provider",
|
||||
|
|
@ -251,6 +283,7 @@ describe("resolvePluginProviders", () => {
|
|||
const registry = createEmptyPluginRegistry();
|
||||
registry.providers.push({ pluginId: "google", provider, source: "bundled" });
|
||||
resolveRuntimePluginRegistryMock.mockReturnValue(registry);
|
||||
loadOpenClawPluginsMock.mockReturnValue(registry);
|
||||
loadPluginManifestRegistryMock.mockReset();
|
||||
applyPluginAutoEnableMock.mockReset();
|
||||
applyPluginAutoEnableMock.mockImplementation(
|
||||
|
|
@ -452,6 +485,43 @@ describe("resolvePluginProviders", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("loads all discovered provider plugins in setup mode", () => {
|
||||
resolvePluginProviders({
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["openrouter"],
|
||||
entries: {
|
||||
google: { enabled: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
mode: "setup",
|
||||
});
|
||||
|
||||
expectLastSetupRegistryLoad({
|
||||
onlyPluginIds: ["google", "kilocode", "moonshot", "workspace-provider"],
|
||||
});
|
||||
expect(getLastSetupLoadedPluginConfig()).toEqual(
|
||||
expect.objectContaining({
|
||||
plugins: expect.objectContaining({
|
||||
allow: expect.arrayContaining([
|
||||
"openrouter",
|
||||
"google",
|
||||
"kilocode",
|
||||
"moonshot",
|
||||
"workspace-provider",
|
||||
]),
|
||||
entries: expect.objectContaining({
|
||||
google: { enabled: true },
|
||||
kilocode: { enabled: true },
|
||||
moonshot: { enabled: true },
|
||||
"workspace-provider": { enabled: true },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("loads provider plugins from the auto-enabled config snapshot", () => {
|
||||
const { rawConfig, autoEnabledConfig } = createAutoEnabledProviderConfig();
|
||||
applyPluginAutoEnableMock.mockReturnValue({
|
||||
|
|
|
|||
|
|
@ -69,8 +69,29 @@ export function resolveEnabledProviderPluginIds(params: {
|
|||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function resolveDiscoveredProviderPluginIds(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
onlyPluginIds?: readonly string[];
|
||||
}): string[] {
|
||||
const onlyPluginIdSet = params.onlyPluginIds ? new Set(params.onlyPluginIds) : null;
|
||||
return loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
})
|
||||
.plugins.filter(
|
||||
(plugin) =>
|
||||
plugin.providers.length > 0 && (!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)),
|
||||
)
|
||||
.map((plugin) => plugin.id)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
resolveEnabledProviderPluginIds,
|
||||
resolveDiscoveredProviderPluginIds,
|
||||
resolveBundledProviderCompatPluginIds,
|
||||
withBundledProviderVitestCompat,
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -345,6 +345,32 @@ describe("resolvePluginWebSearchProviders", () => {
|
|||
expectLoaderCallCount(1);
|
||||
});
|
||||
|
||||
it("loads manifest-declared web-search providers in setup mode", () => {
|
||||
const providers = resolvePluginWebSearchProviders({
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["perplexity"],
|
||||
},
|
||||
},
|
||||
mode: "setup",
|
||||
});
|
||||
|
||||
expect(toRuntimeProviderKeys(providers)).toEqual(["brave:brave"]);
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["brave"],
|
||||
config: expect.objectContaining({
|
||||
plugins: expect.objectContaining({
|
||||
allow: ["perplexity", "brave"],
|
||||
entries: {
|
||||
brave: { enabled: true },
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("loads plugin web-search providers from the auto-enabled config snapshot", () => {
|
||||
const rawConfig = createBraveAllowConfig();
|
||||
const autoEnabledConfig = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import { withActivatedPluginIds } from "./activation-context.js";
|
||||
import {
|
||||
buildPluginSnapshotCacheEnvKey,
|
||||
resolvePluginSnapshotCacheTtlMs,
|
||||
|
|
@ -155,8 +156,36 @@ export function resolvePluginWebSearchProviders(params: {
|
|||
onlyPluginIds?: readonly string[];
|
||||
activate?: boolean;
|
||||
cache?: boolean;
|
||||
mode?: "runtime" | "setup";
|
||||
}): PluginWebSearchProviderEntry[] {
|
||||
const env = params.env ?? process.env;
|
||||
if (params.mode === "setup") {
|
||||
const pluginIds =
|
||||
resolveWebSearchCandidatePluginIds({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
}) ?? [];
|
||||
if (pluginIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const registry = loadOpenClawPlugins({
|
||||
config: withActivatedPluginIds({
|
||||
config: params.config,
|
||||
pluginIds,
|
||||
}),
|
||||
activationSourceConfig: params.config,
|
||||
autoEnabledReasons: {},
|
||||
workspaceDir: params.workspaceDir,
|
||||
env,
|
||||
onlyPluginIds: pluginIds,
|
||||
cache: params.cache ?? false,
|
||||
activate: params.activate ?? false,
|
||||
logger: createPluginLoaderLogger(log),
|
||||
});
|
||||
return mapRegistryWebSearchProviders({ registry, onlyPluginIds: pluginIds });
|
||||
}
|
||||
const cacheOwnerConfig = params.config;
|
||||
const shouldMemoizeSnapshot =
|
||||
params.activate !== true && params.cache !== true && shouldUsePluginSnapshotCache(env);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const loadPluginManifestRegistry = vi.hoisted(() =>
|
||||
vi.fn(() => ({ plugins: [], diagnostics: [] })),
|
||||
);
|
||||
|
||||
vi.mock("../plugins/manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry,
|
||||
}));
|
||||
|
||||
describe("provider env vars dynamic manifest metadata", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
loadPluginManifestRegistry.mockReset();
|
||||
loadPluginManifestRegistry.mockReturnValue({ plugins: [], diagnostics: [] });
|
||||
});
|
||||
|
||||
it("includes later-installed plugin env vars without a bundled generated map", async () => {
|
||||
loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "external-fireworks",
|
||||
origin: "global",
|
||||
providerAuthEnvVars: {
|
||||
fireworks: ["FIREWORKS_ALT_API_KEY"],
|
||||
},
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const mod = await import("./provider-env-vars.js");
|
||||
|
||||
expect(mod.getProviderEnvVars("fireworks")).toEqual(["FIREWORKS_ALT_API_KEY"]);
|
||||
expect(mod.listKnownProviderAuthEnvVarNames()).toContain("FIREWORKS_ALT_API_KEY");
|
||||
expect(mod.listKnownSecretEnvVarNames()).toContain("FIREWORKS_ALT_API_KEY");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES } from "../plugins/bundled-provider-auth-env-vars.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
|
||||
const CORE_PROVIDER_AUTH_ENV_VAR_CANDIDATES = {
|
||||
voyage: ["VOYAGE_API_KEY"],
|
||||
|
|
@ -11,6 +12,73 @@ const CORE_PROVIDER_SETUP_ENV_VAR_OVERRIDES = {
|
|||
"minimax-cn": ["MINIMAX_API_KEY"],
|
||||
} as const;
|
||||
|
||||
type ProviderEnvVarLookupParams = {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
function appendUniqueEnvVarCandidates(
|
||||
target: Record<string, string[]>,
|
||||
providerId: string,
|
||||
keys: readonly string[],
|
||||
) {
|
||||
const normalizedProviderId = providerId.trim();
|
||||
if (!normalizedProviderId || keys.length === 0) {
|
||||
return;
|
||||
}
|
||||
const bucket = (target[normalizedProviderId] ??= []);
|
||||
const seen = new Set(bucket);
|
||||
for (const key of keys) {
|
||||
const normalizedKey = key.trim();
|
||||
if (!normalizedKey || seen.has(normalizedKey)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(normalizedKey);
|
||||
bucket.push(normalizedKey);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveManifestProviderAuthEnvVarCandidates(
|
||||
params?: ProviderEnvVarLookupParams,
|
||||
): Record<string, string[]> {
|
||||
const registry = loadPluginManifestRegistry({
|
||||
config: params?.config,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
env: params?.env,
|
||||
});
|
||||
const candidates: Record<string, string[]> = Object.create(null) as Record<string, string[]>;
|
||||
for (const plugin of registry.plugins) {
|
||||
if (!plugin.providerAuthEnvVars) {
|
||||
continue;
|
||||
}
|
||||
for (const [providerId, keys] of Object.entries(plugin.providerAuthEnvVars).toSorted(
|
||||
([left], [right]) => left.localeCompare(right),
|
||||
)) {
|
||||
appendUniqueEnvVarCandidates(candidates, providerId, keys);
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
export function resolveProviderAuthEnvVarCandidates(
|
||||
params?: ProviderEnvVarLookupParams,
|
||||
): Record<string, readonly string[]> {
|
||||
return {
|
||||
...resolveManifestProviderAuthEnvVarCandidates(params),
|
||||
...CORE_PROVIDER_AUTH_ENV_VAR_CANDIDATES,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveProviderEnvVars(
|
||||
params?: ProviderEnvVarLookupParams,
|
||||
): Record<string, readonly string[]> {
|
||||
return {
|
||||
...resolveProviderAuthEnvVarCandidates(params),
|
||||
...CORE_PROVIDER_SETUP_ENV_VAR_OVERRIDES,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider auth env candidates used by generic auth resolution.
|
||||
*
|
||||
|
|
@ -19,8 +87,7 @@ const CORE_PROVIDER_SETUP_ENV_VAR_OVERRIDES = {
|
|||
* metadata so auth probes do not need to load plugin runtime.
|
||||
*/
|
||||
export const PROVIDER_AUTH_ENV_VAR_CANDIDATES: Record<string, readonly string[]> = {
|
||||
...BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES,
|
||||
...CORE_PROVIDER_AUTH_ENV_VAR_CANDIDATES,
|
||||
...resolveProviderAuthEnvVarCandidates(),
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -33,39 +100,36 @@ export const PROVIDER_AUTH_ENV_VAR_CANDIDATES: Record<string, readonly string[]>
|
|||
* overrides where generic onboarding wants a different preferred env var.
|
||||
*/
|
||||
export const PROVIDER_ENV_VARS: Record<string, readonly string[]> = {
|
||||
...PROVIDER_AUTH_ENV_VAR_CANDIDATES,
|
||||
...CORE_PROVIDER_SETUP_ENV_VAR_OVERRIDES,
|
||||
...resolveProviderEnvVars(),
|
||||
};
|
||||
|
||||
export function getProviderEnvVars(providerId: string): string[] {
|
||||
const envVars = Object.hasOwn(PROVIDER_ENV_VARS, providerId)
|
||||
? PROVIDER_ENV_VARS[providerId]
|
||||
export function getProviderEnvVars(
|
||||
providerId: string,
|
||||
params?: ProviderEnvVarLookupParams,
|
||||
): string[] {
|
||||
const providerEnvVars = resolveProviderEnvVars(params);
|
||||
const envVars = Object.hasOwn(providerEnvVars, providerId)
|
||||
? providerEnvVars[providerId]
|
||||
: undefined;
|
||||
return Array.isArray(envVars) ? [...envVars] : [];
|
||||
}
|
||||
|
||||
const EXTRA_PROVIDER_AUTH_ENV_VARS = ["MINIMAX_CODE_PLAN_KEY", "MINIMAX_CODING_API_KEY"] as const;
|
||||
|
||||
const KNOWN_SECRET_ENV_VARS = [
|
||||
...new Set(Object.values(PROVIDER_ENV_VARS).flatMap((keys) => keys)),
|
||||
];
|
||||
|
||||
// OPENCLAW_API_KEY authenticates the local OpenClaw bridge itself and must
|
||||
// remain available to child bridge/runtime processes.
|
||||
const KNOWN_PROVIDER_AUTH_ENV_VARS = [
|
||||
...new Set([
|
||||
...Object.values(PROVIDER_AUTH_ENV_VAR_CANDIDATES).flatMap((keys) => keys),
|
||||
...KNOWN_SECRET_ENV_VARS,
|
||||
...EXTRA_PROVIDER_AUTH_ENV_VARS,
|
||||
]),
|
||||
];
|
||||
|
||||
export function listKnownProviderAuthEnvVarNames(): string[] {
|
||||
return [...KNOWN_PROVIDER_AUTH_ENV_VARS];
|
||||
export function listKnownProviderAuthEnvVarNames(params?: ProviderEnvVarLookupParams): string[] {
|
||||
return [
|
||||
...new Set([
|
||||
...Object.values(resolveProviderAuthEnvVarCandidates(params)).flatMap((keys) => keys),
|
||||
...Object.values(resolveProviderEnvVars(params)).flatMap((keys) => keys),
|
||||
...EXTRA_PROVIDER_AUTH_ENV_VARS,
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
export function listKnownSecretEnvVarNames(): string[] {
|
||||
return [...KNOWN_SECRET_ENV_VARS];
|
||||
export function listKnownSecretEnvVarNames(params?: ProviderEnvVarLookupParams): string[] {
|
||||
return [...new Set(Object.values(resolveProviderEnvVars(params)).flatMap((keys) => keys))];
|
||||
}
|
||||
|
||||
export function omitEnvKeysCaseInsensitive(
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ vi.mock("../commands/health.js", () => ({
|
|||
}));
|
||||
|
||||
vi.mock("../commands/onboard-search.js", () => ({
|
||||
SEARCH_PROVIDER_OPTIONS: [],
|
||||
listSearchProviderOptions: () => [],
|
||||
resolveSearchProviderOptions: () => [],
|
||||
hasExistingKey,
|
||||
hasKeyInEnv,
|
||||
|
|
|
|||
|
|
@ -48,8 +48,7 @@ async function resolveAuthChoiceModelSelectionPolicy(params: {
|
|||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
mode: "setup",
|
||||
});
|
||||
const resolvedChoice = resolveProviderPluginChoice({
|
||||
providers,
|
||||
|
|
|
|||
|
|
@ -51,14 +51,16 @@ describe("app-tool-stream fallback lifecycle handling", () => {
|
|||
data: {
|
||||
phase: "fallback",
|
||||
selectedProvider: "fireworks",
|
||||
selectedModel: "fireworks/minimax-m2p5",
|
||||
selectedModel: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
activeProvider: "deepinfra",
|
||||
activeModel: "moonshotai/Kimi-K2.5",
|
||||
reasonSummary: "rate limit",
|
||||
},
|
||||
});
|
||||
|
||||
expect(host.fallbackStatus?.selected).toBe("fireworks/minimax-m2p5");
|
||||
expect(host.fallbackStatus?.selected).toBe(
|
||||
"fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
);
|
||||
expect(host.fallbackStatus?.active).toBe("deepinfra/moonshotai/Kimi-K2.5");
|
||||
expect(host.fallbackStatus?.reason).toBe("rate limit");
|
||||
vi.useRealTimers();
|
||||
|
|
@ -77,7 +79,7 @@ describe("app-tool-stream fallback lifecycle handling", () => {
|
|||
data: {
|
||||
phase: "fallback",
|
||||
selectedProvider: "fireworks",
|
||||
selectedModel: "fireworks/minimax-m2p5",
|
||||
selectedModel: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
activeProvider: "deepinfra",
|
||||
activeModel: "moonshotai/Kimi-K2.5",
|
||||
},
|
||||
|
|
@ -100,7 +102,7 @@ describe("app-tool-stream fallback lifecycle handling", () => {
|
|||
data: {
|
||||
phase: "fallback",
|
||||
selectedProvider: "fireworks",
|
||||
selectedModel: "fireworks/minimax-m2p5",
|
||||
selectedModel: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
activeProvider: "deepinfra",
|
||||
activeModel: "moonshotai/Kimi-K2.5",
|
||||
},
|
||||
|
|
@ -127,9 +129,9 @@ describe("app-tool-stream fallback lifecycle handling", () => {
|
|||
data: {
|
||||
phase: "fallback_cleared",
|
||||
selectedProvider: "fireworks",
|
||||
selectedModel: "fireworks/minimax-m2p5",
|
||||
selectedModel: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
activeProvider: "fireworks",
|
||||
activeModel: "fireworks/minimax-m2p5",
|
||||
activeModel: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
previousActiveProvider: "deepinfra",
|
||||
previousActiveModel: "moonshotai/Kimi-K2.5",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -622,9 +622,9 @@ describe("chat view", () => {
|
|||
renderChat(
|
||||
createProps({
|
||||
fallbackStatus: {
|
||||
selected: "fireworks/minimax-m2p5",
|
||||
selected: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
active: "deepinfra/moonshotai/Kimi-K2.5",
|
||||
attempts: ["fireworks/minimax-m2p5: rate limit"],
|
||||
attempts: ["fireworks/accounts/fireworks/routers/kimi-k2p5-turbo: rate limit"],
|
||||
occurredAt: 900,
|
||||
},
|
||||
}),
|
||||
|
|
@ -645,7 +645,7 @@ describe("chat view", () => {
|
|||
renderChat(
|
||||
createProps({
|
||||
fallbackStatus: {
|
||||
selected: "fireworks/minimax-m2p5",
|
||||
selected: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
active: "deepinfra/moonshotai/Kimi-K2.5",
|
||||
attempts: [],
|
||||
occurredAt: 0,
|
||||
|
|
@ -667,8 +667,8 @@ describe("chat view", () => {
|
|||
createProps({
|
||||
fallbackStatus: {
|
||||
phase: "cleared",
|
||||
selected: "fireworks/minimax-m2p5",
|
||||
active: "fireworks/minimax-m2p5",
|
||||
selected: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
active: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
previous: "deepinfra/moonshotai/Kimi-K2.5",
|
||||
attempts: [],
|
||||
occurredAt: 900,
|
||||
|
|
@ -680,7 +680,9 @@ describe("chat view", () => {
|
|||
|
||||
const indicator = container.querySelector(".compaction-indicator--fallback-cleared");
|
||||
expect(indicator).not.toBeNull();
|
||||
expect(indicator?.textContent).toContain("Fallback cleared: fireworks/minimax-m2p5");
|
||||
expect(indicator?.textContent).toContain(
|
||||
"Fallback cleared: fireworks/accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||
);
|
||||
nowSpy.mockRestore();
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue