



























@@ -5,6 +5,8 @@ import { describe, expect, it } from "vitest";
55import { getRuntimeConfig } from "../config/config.js";
66import type { OpenClawConfig } from "../config/types.openclaw.js";
77import { parseLiveCsvFilter } from "../media-generation/live-test-helpers.js";
8+import { withBundledPluginEnablementCompat } from "../plugins/bundled-compat.js";
9+import { resolveOwningPluginIdsForProviderRef } from "../plugins/providers.js";
810import { runTasksWithConcurrency } from "../utils/run-with-concurrency.js";
911import {
1012discoverAuthStorage,
@@ -60,6 +62,7 @@ import {
6062import { getApiKeyForModel, requireApiKey } from "./model-auth.js";
6163import { shouldSuppressBuiltInModel } from "./model-suppression.js";
6264import { ensureOpenClawModelsJson } from "./models-config.js";
65+import { normalizeProviderId } from "./provider-id.js";
6366import { prepareModelForSimpleCompletion } from "./simple-completion-transport.js";
64676568const LIVE = isLiveTestEnabled();
@@ -99,6 +102,118 @@ function parseModelFilter(raw?: string): Set<string> | null {
99102return parseCsvFilter(raw);
100103}
101104105+function parseExplicitLiveModelRefs(
106+filter: Set<string> | null,
107+): Array<{ provider: string; id: string }> {
108+if (!filter) {
109+return [];
110+}
111+const refs: Array<{ provider: string; id: string }> = [];
112+const seen = new Set<string>();
113+for (const raw of filter) {
114+const trimmed = raw.trim();
115+const slash = trimmed.indexOf("/");
116+if (slash <= 0 || slash === trimmed.length - 1) {
117+continue;
118+}
119+const provider = normalizeProviderId(trimmed.slice(0, slash));
120+const id = trimmed.slice(slash + 1).trim();
121+if (!provider || !id) {
122+continue;
123+}
124+const key = `${provider}\0${id.toLowerCase()}`;
125+if (seen.has(key)) {
126+continue;
127+}
128+seen.add(key);
129+refs.push({ provider, id });
130+}
131+return refs;
132+}
133+134+function formatExplicitLiveModelRef(ref: { provider: string; id: string }): string {
135+return `${ref.provider}/${ref.id}`;
136+}
137+138+function findUnmatchedExplicitLiveModelRefs(params: {
139+refs: readonly { provider: string; id: string }[];
140+models: readonly Pick<Model, "provider" | "id">[];
141+config?: OpenClawConfig;
142+env?: NodeJS.ProcessEnv;
143+}): string[] {
144+const unmatched: string[] = [];
145+for (const ref of params.refs) {
146+const matcher = createLiveTargetMatcher({
147+providerFilter: null,
148+modelFilter: new Set([formatExplicitLiveModelRef(ref)]),
149+config: params.config,
150+env: params.env,
151+});
152+const matched = params.models.some((model) => matcher.matchesModel(model.provider, model.id));
153+if (!matched) {
154+unmatched.push(formatExplicitLiveModelRef(ref));
155+}
156+}
157+return unmatched;
158+}
159+160+function resolveLiveProviderDiscoveryProviderIds(params: {
161+providerFilter: Set<string> | null;
162+explicitRefs: readonly { provider: string; id: string }[];
163+}): string[] | undefined {
164+const providers = new Set<string>();
165+for (const provider of params.providerFilter ?? []) {
166+const normalized = normalizeProviderId(provider);
167+if (normalized) {
168+providers.add(normalized);
169+}
170+}
171+for (const ref of params.explicitRefs) {
172+providers.add(ref.provider);
173+}
174+return providers.size > 0
175+ ? [...providers].toSorted((left, right) => left.localeCompare(right))
176+ : undefined;
177+}
178+179+function resolveLiveProviderDiscoveryPluginIds(params: {
180+config?: OpenClawConfig;
181+providers: readonly string[] | undefined;
182+env?: NodeJS.ProcessEnv;
183+}): string[] {
184+const pluginIds = new Set<string>();
185+for (const provider of params.providers ?? []) {
186+const owners =
187+resolveOwningPluginIdsForProviderRef({
188+ provider,
189+config: params.config,
190+env: params.env,
191+}) ?? [];
192+if (owners.length === 0) {
193+pluginIds.add(provider);
194+continue;
195+}
196+for (const owner of owners) {
197+pluginIds.add(owner);
198+}
199+}
200+return [...pluginIds].toSorted((left, right) => left.localeCompare(right));
201+}
202+203+function applyLiveProviderDiscoveryPluginCompat(params: {
204+config: OpenClawConfig;
205+providers: readonly string[] | undefined;
206+env?: NodeJS.ProcessEnv;
207+}): OpenClawConfig {
208+const pluginIds = resolveLiveProviderDiscoveryPluginIds(params);
209+return pluginIds.length > 0
210+ ? (withBundledPluginEnablementCompat({
211+config: params.config,
212+ pluginIds,
213+}) ?? params.config)
214+ : params.config;
215+}
216+102217function logProgress(message: string): void {
103218writeSync(2, `[live] ${message}\n`);
104219}
@@ -358,6 +473,70 @@ describe("resolveLiveModelsJsonTimeoutMs", () => {
358473});
359474});
360475476+describe("explicit live model discovery scope", () => {
477+it("derives provider ids from explicit model refs", () => {
478+const filter = parseModelFilter(
479+"zai/glm-5.1, together/Qwen/Qwen2.5-7B-Instruct-Turbo, glm-5.1",
480+);
481+const explicitRefs = parseExplicitLiveModelRefs(filter);
482+483+expect(explicitRefs).toEqual([
484+{ provider: "zai", id: "glm-5.1" },
485+{ provider: "together", id: "Qwen/Qwen2.5-7B-Instruct-Turbo" },
486+]);
487+expect(
488+resolveLiveProviderDiscoveryProviderIds({
489+providerFilter: null,
490+ explicitRefs,
491+}),
492+).toEqual(["together", "zai"]);
493+});
494+495+it("merges explicit model providers with OPENCLAW_LIVE_PROVIDERS", () => {
496+const explicitRefs = parseExplicitLiveModelRefs(parseModelFilter("zai/glm-5.1"));
497+498+expect(
499+resolveLiveProviderDiscoveryProviderIds({
500+providerFilter: parseProviderFilter("deepseek,together"),
501+ explicitRefs,
502+}),
503+).toEqual(["deepseek", "together", "zai"]);
504+});
505+506+it("activates bundled provider plugins for explicit live discovery", () => {
507+const cfg = {
508+plugins: {
509+allow: ["openai"],
510+bundledDiscovery: "compat",
511+entries: {
512+openai: { enabled: true },
513+},
514+},
515+} satisfies OpenClawConfig;
516+517+expect(
518+applyLiveProviderDiscoveryPluginCompat({
519+config: cfg,
520+providers: ["deepseek"],
521+env: {},
522+}).plugins?.entries?.deepseek,
523+).toEqual({ enabled: true });
524+});
525+526+it("reports explicit refs that never become runnable candidates", () => {
527+expect(
528+findUnmatchedExplicitLiveModelRefs({
529+refs: [
530+{ provider: "deepseek", id: "deepseek-v4-flash" },
531+{ provider: "zai", id: "glm-5.1" },
532+],
533+models: [{ provider: "deepseek", id: "deepseek-v4-flash" }],
534+env: {},
535+}),
536+).toEqual(["zai/glm-5.1"]);
537+});
538+});
539+361540function resolveTestReasoning(
362541model: Model,
363542): "minimal" | "low" | "medium" | "high" | "xhigh" | undefined {
@@ -724,14 +903,34 @@ describeLive("live models (profile keys)", () => {
724903"completes across selected models",
725904async () => {
726905logProgress("[live-models] loading config");
727-const cfg = await withLiveStageTimeout(
906+const loadedCfg = await withLiveStageTimeout(
728907Promise.resolve().then(() => getRuntimeConfig()),
729908"[live-models] load config",
730909);
910+const rawModels = process.env.OPENCLAW_LIVE_MODELS?.trim();
911+const useModern = rawModels === "modern" || rawModels === "all";
912+const useSmall = rawModels === "small";
913+const useExplicit = Boolean(rawModels) && !useModern && !useSmall;
914+const filter = useExplicit ? parseModelFilter(rawModels) : null;
915+const explicitRefs = useExplicit ? parseExplicitLiveModelRefs(filter) : [];
916+const providers = parseProviderFilter(process.env.OPENCLAW_LIVE_PROVIDERS);
917+const providerList = resolveLiveProviderDiscoveryProviderIds({
918+providerFilter: providers,
919+ explicitRefs,
920+});
921+const cfg = applyLiveProviderDiscoveryPluginCompat({
922+config: loadedCfg,
923+providers: providerList,
924+env: process.env,
925+});
731926activeLiveCompletionConfig = cfg;
732927logProgress("[live-models] preparing models.json");
733928await withLiveStageTimeout(
734-ensureOpenClawModelsJson(cfg),
929+ensureOpenClawModelsJson(
930+cfg,
931+undefined,
932+providerList ? { providerDiscoveryProviderIds: providerList } : undefined,
933+),
735934"[live-models] prepare models.json",
736935LIVE_MODELS_JSON_TIMEOUT_MS,
737936);
@@ -747,15 +946,8 @@ describeLive("live models (profile keys)", () => {
747946logProgress(`[live-models] anthropic keys loaded: ${anthropicKeys.length}`);
748947}
749948750-const providers = parseProviderFilter(process.env.OPENCLAW_LIVE_PROVIDERS);
751-const providerList = providers ? [...providers] : null;
752949logProgress("[live-models] resolving agent dir");
753950const agentDir = resolveDefaultAgentDir(cfg);
754-const rawModels = process.env.OPENCLAW_LIVE_MODELS?.trim();
755-const useModern = rawModels === "modern" || rawModels === "all";
756-const useSmall = rawModels === "small";
757-const useExplicit = Boolean(rawModels) && !useModern && !useSmall;
758-const filter = useExplicit ? parseModelFilter(rawModels) : null;
759951const useDefaultPriorityOnly = !filter && useModern && !providers;
760952const useSmallPriorityOnly = !filter && useSmall && !providers;
761953const allowNotFoundSkip = useModern || useSmall;
@@ -797,7 +989,11 @@ describeLive("live models (profile keys)", () => {
797989 agentDir,
798990env: process.env,
799991 modelRegistry,
800- ...(useSmall ? { refs: listPrioritizedSmallLiveModelRefs() } : {}),
992+ ...(explicitRefs.length > 0
993+ ? { refs: explicitRefs }
994+ : useSmall
995+ ? { refs: listPrioritizedSmallLiveModelRefs() }
996+ : {}),
801997});
802998if (augmented.added.length > 0) {
803999logProgress(
@@ -908,6 +1104,21 @@ describeLive("live models (profile keys)", () => {
9081104logProgress("[live-models] no API keys found; skipping");
9091105return;
9101106}
1107+if (useExplicit && explicitRefs.length > 0) {
1108+const unmatched = findUnmatchedExplicitLiveModelRefs({
1109+refs: explicitRefs,
1110+models: candidates.map((entry) => entry.model),
1111+config: cfg,
1112+env: process.env,
1113+});
1114+if (unmatched.length > 0) {
1115+const skippedPreview =
1116+skipped.length > 0 ? `\nSkipped candidates:\n${formatSkippedPreview(skipped, 8)}` : "";
1117+throw new Error(
1118+`[live-models] explicit model selection missed requested models: ${unmatched.join(", ")}.${skippedPreview}`,
1119+);
1120+}
1121+}
91111229121123const selectCandidates = useSmall ? selectSmallLiveItems : selectHighSignalLiveItems;
9131124const selectedCandidates = selectCandidates(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。