diff --git a/CHANGELOG.md b/CHANGELOG.md index 74da3b40933..9e6b3777282 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/providers/fireworks.md b/docs/providers/fireworks.md new file mode 100644 index 00000000000..92ceaa467f5 --- /dev/null +++ b/docs/providers/fireworks.md @@ -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. diff --git a/docs/providers/index.md b/docs/providers/index.md index f06dd0edb66..dbd5d31b462 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -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) diff --git a/docs/providers/models.md b/docs/providers/models.md index 7235b5935eb..d312fa0e336 100644 --- a/docs/providers/models.md +++ b/docs/providers/models.md @@ -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) diff --git a/extensions/fireworks/index.test.ts b/extensions/fireworks/index.test.ts new file mode 100644 index 00000000000..38fed9883ee --- /dev/null +++ b/extensions/fireworks/index.test.ts @@ -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, + }); + }); +}); diff --git a/extensions/fireworks/index.ts b/extensions/fireworks/index.ts new file mode 100644 index 00000000000..fad2a8386a0 --- /dev/null +++ b/extensions/fireworks/index.ts @@ -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, + }, +}); diff --git a/extensions/fireworks/onboard.ts b/extensions/fireworks/onboard.ts new file mode 100644 index 00000000000..91ece43cbe3 --- /dev/null +++ b/extensions/fireworks/onboard.ts @@ -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); +} diff --git a/extensions/fireworks/openclaw.plugin.json b/extensions/fireworks/openclaw.plugin.json new file mode 100644 index 00000000000..2837f34f1b8 --- /dev/null +++ b/extensions/fireworks/openclaw.plugin.json @@ -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 ", + "cliDescription": "Fireworks API key" + } + ], + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": {} + } +} diff --git a/extensions/fireworks/package.json b/extensions/fireworks/package.json new file mode 100644 index 00000000000..2c1ce2fce31 --- /dev/null +++ b/extensions/fireworks/package.json @@ -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" + ] + } +} diff --git a/extensions/fireworks/provider-catalog.ts b/extensions/fireworks/provider-catalog.ts new file mode 100644 index 00000000000..f5bb19b0f9a --- /dev/null +++ b/extensions/fireworks/provider-catalog.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(), + }; +} diff --git a/package.json b/package.json index 8464b85a1b5..c6725564322 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/check-web-fetch-provider-boundaries.mjs b/scripts/check-web-fetch-provider-boundaries.mjs index 2d39159f4c8..0b5bd41532b 100644 --- a/scripts/check-web-fetch-provider-boundaries.mjs +++ b/scripts/check-web-fetch-provider-boundaries.mjs @@ -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 = [ diff --git a/scripts/generate-bundled-provider-auth-env-vars.mjs b/scripts/generate-bundled-provider-auth-env-vars.mjs deleted file mode 100644 index d4c3a279a31..00000000000 --- a/scripts/generate-bundled-provider-auth-env-vars.mjs +++ /dev/null @@ -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; -`; -} - -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 }), -}); diff --git a/src/agents/model-auth-env-vars.ts b/src/agents/model-auth-env-vars.ts index e318cd2e9c8..00ba3b4e7c8 100644 --- a/src/agents/model-auth-env-vars.ts +++ b/src/agents/model-auth-env-vars.ts @@ -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 { + return resolveProviderAuthEnvVarCandidates(); +} + +export const PROVIDER_ENV_API_KEY_CANDIDATES = resolveProviderEnvApiKeyCandidates(); export function listKnownProviderEnvApiKeyNames(): string[] { return listKnownProviderAuthEnvVarNames(); diff --git a/src/agents/model-auth-env.ts b/src/agents/model-auth-env.ts index a0866cb51fd..37ada8503c0 100644 --- a/src/agents/model-auth-env.ts +++ b/src/agents/model-auth-env.ts @@ -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); diff --git a/src/agents/pi-model-discovery.ts b/src/agents/pi-model-discovery.ts index 251cece7149..2e3255c3729 100644 --- a/src/agents/pi-model-discovery.ts +++ b/src/agents/pi-model-discovery.ts @@ -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; } diff --git a/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts b/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts index f86a7fd93f0..1328d499257 100644 --- a/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts +++ b/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts @@ -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", }; diff --git a/src/auto-reply/reply/directive-handling.model.test.ts b/src/auto-reply/reply/directive-handling.model.test.ts index 4cda520e4a4..7f9237a3366 100644 --- a/src/auto-reply/reply/directive-handling.model.test.ts +++ b/src/auto-reply/reply/directive-handling.model.test.ts @@ -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)"); }); diff --git a/src/auto-reply/status.test.ts b/src/auto-reply/status.test.ts index 04b57313969..a810e133a6c 100644 --- a/src/auto-reply/status.test.ts +++ b/src/auto-reply/status.test.ts @@ -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", }, diff --git a/src/commands/auth-choice-options.test.ts b/src/commands/auth-choice-options.test.ts index e8c595a443c..87b2eff06a6 100644 --- a/src/commands/auth-choice-options.test.ts +++ b/src/commands/auth-choice-options.test.ts @@ -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", () => { diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 2f015cb6132..cc57d3f6085 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -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), diff --git a/src/commands/configure.gateway-auth.ts b/src/commands/configure.gateway-auth.ts index 3bec628ecee..3af53e463a9 100644 --- a/src/commands/configure.gateway-auth.ts +++ b/src/commands/configure.gateway-auth.ts @@ -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, diff --git a/src/commands/doctor-auth.ts b/src/commands/doctor-auth.ts index 3df000fc138..a12e162c36f 100644 --- a/src/commands/doctor-auth.ts +++ b/src/commands/doctor-auth.ts @@ -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 ?? []) diff --git a/src/commands/model-picker.test.ts b/src/commands/model-picker.test.ts index 0785627e8b5..903171bb865 100644 --- a/src/commands/model-picker.test.ts +++ b/src/commands/model-picker.test.ts @@ -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({ diff --git a/src/commands/models/auth.ts b/src/commands/models/auth.ts index 487d4ef6c1e..a563d4bddc4 100644 --- a/src/commands/models/auth.ts +++ b/src/commands/models/auth.ts @@ -110,8 +110,7 @@ async function resolveModelsAuthContext(): Promise { const providers = resolvePluginProviders({ config, workspaceDir, - bundledProviderAllowlistCompat: true, - bundledProviderVitestCompat: true, + mode: "setup", }); return { config, agentDir, workspaceDir, providers }; } diff --git a/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts index e97f55a286d..3386d957067 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts @@ -110,8 +110,7 @@ export async function applyNonInteractivePluginProviderChoice(params: { config: resolutionConfig, workspaceDir, onlyPluginIds: owningPluginIds, - bundledProviderAllowlistCompat: true, - bundledProviderVitestCompat: true, + mode: "setup", }), choice: params.authChoice, }); diff --git a/src/commands/onboard-search.providers.test.ts b/src/commands/onboard-search.providers.test.ts index c62360d335c..ba2e4377675 100644 --- a/src/commands/onboard-search.providers.test.ts +++ b/src/commands/onboard-search.providers.test.ts @@ -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", + ]); }); }); diff --git a/src/commands/onboard-search.test.ts b/src/commands/onboard-search.test.ts index f974173afa4..879df362c1f 100644 --- a/src/commands/onboard-search.test.ts +++ b/src/commands/onboard-search.test.ts @@ -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", diff --git a/src/commands/onboard-search.ts b/src/commands/onboard-search.ts index 56ecc009443..66d8eb09f14 100644 --- a/src/commands/onboard-search.ts +++ b/src/commands/onboard-search.ts @@ -1,9 +1,9 @@ export { - SEARCH_PROVIDER_OPTIONS, applySearchKey, applySearchProviderSelection, hasExistingKey, hasKeyInEnv, + listSearchProviderOptions, resolveExistingKey, resolveSearchProviderOptions, runSearchSetupFlow as setupSearch, diff --git a/src/commands/provider-auth-guidance.ts b/src/commands/provider-auth-guidance.ts index 30a907e9cff..aaa17dc5bb3 100644 --- a/src/commands/provider-auth-guidance.ts +++ b/src/commands/provider-auth-guidance.ts @@ -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; diff --git a/src/flows/model-picker.ts b/src/flows/model-picker.ts index e7ba059b157..6d5b469307d 100644 --- a/src/flows/model-picker.ts +++ b/src/flows/model-picker.ts @@ -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({ diff --git a/src/flows/provider-flow.test.ts b/src/flows/provider-flow.test.ts index 97a8a035778..a2ba0d9bb56 100644 --- a/src/flows/provider-flow.test.ts +++ b/src/flows/provider-flow.test.ts @@ -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" }); }); diff --git a/src/flows/provider-flow.ts b/src/flows/provider-flow.ts index 761cd7cff9b..62612457b88 100644 --- a/src/flows/provider-flow.ts +++ b/src/flows/provider-flow.ts @@ -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, + })), ); } diff --git a/src/flows/search-setup.test.ts b/src/flows/search-setup.test.ts index 2af0a0383aa..f930927f67e 100644 --- a/src/flows/search-setup.test.ts +++ b/src/flows/search-setup.test.ts @@ -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], })); diff --git a/src/flows/search-setup.ts b/src/flows/search-setup.ts index e17647734c9..c354ec60665 100644 --- a/src/flows/search-setup.ts +++ b/src/flows/search-setup.ts @@ -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, @@ -56,20 +55,6 @@ function showsSearchProviderInSetup( return entry.onboardingScopes?.includes("text-inference") ?? false; } -function canRepairBundledProviderSelection( - config: OpenClawConfig, - provider: Pick, -): 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( + 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( diff --git a/src/gateway/server-chat.agent-events.test.ts b/src/gateway/server-chat.agent-events.test.ts index 959553c81cd..0fd00690593 100644 --- a/src/gateway/server-chat.agent-events.test.ts +++ b/src/gateway/server-chat.agent-events.test.ts @@ -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; diff --git a/src/plugins/activation-context.ts b/src/plugins/activation-context.ts index 94fc4958849..c5fa9c9f194 100644 --- a/src/plugins/activation-context.ts +++ b/src/plugins/activation-context.ts @@ -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; diff --git a/src/plugins/bundled-provider-auth-env-vars.generated.ts b/src/plugins/bundled-provider-auth-env-vars.generated.ts deleted file mode 100644 index a73cc547f30..00000000000 --- a/src/plugins/bundled-provider-auth-env-vars.generated.ts +++ /dev/null @@ -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; diff --git a/src/plugins/bundled-provider-auth-env-vars.test.ts b/src/plugins/bundled-provider-auth-env-vars.test.ts deleted file mode 100644 index cd515318ad4..00000000000 --- a/src/plugins/bundled-provider-auth-env-vars.test.ts +++ /dev/null @@ -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) { - 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, - }); - }); -}); diff --git a/src/plugins/bundled-provider-auth-env-vars.ts b/src/plugins/bundled-provider-auth-env-vars.ts deleted file mode 100644 index 3df3d5c9d36..00000000000 --- a/src/plugins/bundled-provider-auth-env-vars.ts +++ /dev/null @@ -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"; diff --git a/src/plugins/bundled-web-fetch-ids.ts b/src/plugins/bundled-web-fetch-ids.ts index 8747693863c..f7daf3c478c 100644 --- a/src/plugins/bundled-web-fetch-ids.ts +++ b/src/plugins/bundled-web-fetch-ids.ts @@ -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"; diff --git a/src/plugins/bundled-web-fetch-provider-ids.ts b/src/plugins/bundled-web-fetch-provider-ids.ts index 9a82483d8e2..124b2204bab 100644 --- a/src/plugins/bundled-web-fetch-provider-ids.ts +++ b/src/plugins/bundled-web-fetch-provider-ids.ts @@ -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>; - -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"; diff --git a/src/plugins/bundled-web-fetch.ts b/src/plugins/bundled-web-fetch.ts index f342595d7ef..a16a54ee758 100644 --- a/src/plugins/bundled-web-fetch.ts +++ b/src/plugins/bundled-web-fetch.ts @@ -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(); -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(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; } diff --git a/src/plugins/bundled-web-search-ids.ts b/src/plugins/bundled-web-search-ids.ts index bbde719bdf0..493494f1dc9 100644 --- a/src/plugins/bundled-web-search-ids.ts +++ b/src/plugins/bundled-web-search-ids.ts @@ -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(); } diff --git a/src/plugins/bundled-web-search-provider-ids.ts b/src/plugins/bundled-web-search-provider-ids.ts index dfdbebdae3c..45c63e0090d 100644 --- a/src/plugins/bundled-web-search-provider-ids.ts +++ b/src/plugins/bundled-web-search-provider-ids.ts @@ -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"; diff --git a/src/plugins/bundled-web-search.test.ts b/src/plugins/bundled-web-search.test.ts index cba6cf26491..99d60f58ed1 100644 --- a/src/plugins/bundled-web-search.test.ts +++ b/src/plugins/bundled-web-search.test.ts @@ -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", }); }); diff --git a/src/plugins/bundled-web-search.ts b/src/plugins/bundled-web-search.ts index 4f6866753ed..82bb4454dc6 100644 --- a/src/plugins/bundled-web-search.ts +++ b/src/plugins/bundled-web-search.ts @@ -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(); -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(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; } diff --git a/src/plugins/provider-auth-choice-preference.ts b/src/plugins/provider-auth-choice-preference.ts index 20e72541a6c..8bfe7377a81 100644 --- a/src/plugins/provider-auth-choice-preference.ts +++ b/src/plugins/provider-auth-choice-preference.ts @@ -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, diff --git a/src/plugins/provider-auth-choice.ts b/src/plugins/provider-auth-choice.ts index 1654b550a6b..8c893a330d4 100644 --- a/src/plugins/provider-auth-choice.ts +++ b/src/plugins/provider-auth-choice.ts @@ -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) { diff --git a/src/plugins/provider-wizard.test.ts b/src/plugins/provider-wizard.test.ts index 3f0e7e5267b..108a0d9c9f7 100644 --- a/src/plugins/provider-wizard.test.ts +++ b/src/plugins/provider-wizard.test.ts @@ -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 () => {}); diff --git a/src/plugins/provider-wizard.ts b/src/plugins/provider-wizard.ts index 72b73b6a59c..ffa608acb1e 100644 --- a/src/plugins/provider-wizard.ts +++ b/src/plugins/provider-wizard.ts @@ -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> ->(); - -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>(); - providerWizardCache.set(params.config, nextConfigCache); - } - let nextEnvCache = nextConfigCache.get(env); - if (!nextEnvCache) { - nextEnvCache = new Map(); - nextConfigCache.set(env, nextEnvCache); - } - nextEnvCache.set(cacheKey, { - expiresAt: Date.now() + ttlMs, - providers, - }); - return providers; } export function resolveProviderWizardOptions(params: { diff --git a/src/plugins/providers.runtime.ts b/src/plugins/providers.runtime.ts index 90bec1a08ad..7f8cc134ea0 100644 --- a/src/plugins/providers.runtime.ts +++ b/src/plugins/providers.runtime.ts @@ -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, diff --git a/src/plugins/providers.test.ts b/src/plugins/providers.test.ts index 7fb321b9150..30f22ceb39d 100644 --- a/src/plugins/providers.test.ts +++ b/src/plugins/providers.test.ts @@ -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(); +const loadOpenClawPluginsMock = vi.fn(); const loadPluginManifestRegistryMock = vi.fn(); const applyPluginAutoEnableMock = vi.fn(); @@ -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; + }; + } + | 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) => + loadOpenClawPluginsMock(...args), resolveRuntimePluginRegistry: (...args: Parameters) => 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({ diff --git a/src/plugins/providers.ts b/src/plugins/providers.ts index 3bab9095de7..ddf32ea3164 100644 --- a/src/plugins/providers.ts +++ b/src/plugins/providers.ts @@ -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; diff --git a/src/plugins/web-search-providers.runtime.test.ts b/src/plugins/web-search-providers.runtime.test.ts index e90013524ed..8307f75c1a3 100644 --- a/src/plugins/web-search-providers.runtime.test.ts +++ b/src/plugins/web-search-providers.runtime.test.ts @@ -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 = { diff --git a/src/plugins/web-search-providers.runtime.ts b/src/plugins/web-search-providers.runtime.ts index d8b0345caa7..0fb1d27fddd 100644 --- a/src/plugins/web-search-providers.runtime.ts +++ b/src/plugins/web-search-providers.runtime.ts @@ -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); diff --git a/src/secrets/provider-env-vars.dynamic.test.ts b/src/secrets/provider-env-vars.dynamic.test.ts new file mode 100644 index 00000000000..6969eac77b4 --- /dev/null +++ b/src/secrets/provider-env-vars.dynamic.test.ts @@ -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"); + }); +}); diff --git a/src/secrets/provider-env-vars.ts b/src/secrets/provider-env-vars.ts index ccf77344faf..979f69152f3 100644 --- a/src/secrets/provider-env-vars.ts +++ b/src/secrets/provider-env-vars.ts @@ -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, + 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 { + const registry = loadPluginManifestRegistry({ + config: params?.config, + workspaceDir: params?.workspaceDir, + env: params?.env, + }); + const candidates: Record = Object.create(null) as Record; + 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 { + return { + ...resolveManifestProviderAuthEnvVarCandidates(params), + ...CORE_PROVIDER_AUTH_ENV_VAR_CANDIDATES, + }; +} + +export function resolveProviderEnvVars( + params?: ProviderEnvVarLookupParams, +): Record { + 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 = { - ...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 * overrides where generic onboarding wants a different preferred env var. */ export const PROVIDER_ENV_VARS: Record = { - ...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( diff --git a/src/wizard/setup.finalize.test.ts b/src/wizard/setup.finalize.test.ts index a773e8051ec..dbb0f08db01 100644 --- a/src/wizard/setup.finalize.test.ts +++ b/src/wizard/setup.finalize.test.ts @@ -89,7 +89,7 @@ vi.mock("../commands/health.js", () => ({ })); vi.mock("../commands/onboard-search.js", () => ({ - SEARCH_PROVIDER_OPTIONS: [], + listSearchProviderOptions: () => [], resolveSearchProviderOptions: () => [], hasExistingKey, hasKeyInEnv, diff --git a/src/wizard/setup.ts b/src/wizard/setup.ts index 66d6694e6ff..7e722cd95c2 100644 --- a/src/wizard/setup.ts +++ b/src/wizard/setup.ts @@ -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, diff --git a/ui/src/ui/app-tool-stream.node.test.ts b/ui/src/ui/app-tool-stream.node.test.ts index ab6a2fe8c5d..e3d064be245 100644 --- a/ui/src/ui/app-tool-stream.node.test.ts +++ b/ui/src/ui/app-tool-stream.node.test.ts @@ -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", }, diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index 4705574c106..a26b2f1fca3 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -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(); });