

























@@ -3,7 +3,12 @@ import fs from "node:fs/promises";
33import { createServer } from "node:net";
44import os from "node:os";
55import path from "node:path";
6-import type { Api, Model } from "@mariozechner/pi-ai";
6+import {
7+clampThinkingLevel,
8+type Api,
9+type Model,
10+type ModelThinkingLevel,
11+} from "@mariozechner/pi-ai";
712import { afterEach, describe, expect, it } from "vitest";
813import { resolveAgentWorkspaceDir, resolveDefaultAgentDir } from "../agents/agent-scope.js";
914import { ensureAuthProfileStore, saveAuthProfileStore } from "../agents/auth-profiles/store.js";
@@ -36,6 +41,7 @@ import { clearRuntimeConfigSnapshot, getRuntimeConfig } from "../config/io.js";
3641import type { ModelsConfig, ModelProviderConfig, OpenClawConfig } from "../config/types.js";
3742import { isTruthyEnvValue } from "../infra/env.js";
3843import { normalizeGoogleModelId } from "../plugin-sdk/google-model-id.js";
44+import { resolveProviderThinkingProfile } from "../plugins/provider-runtime.js";
3945import { DEFAULT_AGENT_ID } from "../routing/session-key.js";
4046import { stripAssistantInternalScaffolding } from "../shared/text/assistant-visible-text.js";
4147import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
@@ -587,6 +593,95 @@ describe("resolveGatewayLiveMaxModels", () => {
587593});
588594});
589595596+function createGatewayLiveTestModel(provider: string, id: string): Model<Api> {
597+return {
598+ provider,
599+ id,
600+name: id,
601+api: "openai-responses",
602+input: ["text"],
603+cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
604+contextWindow: 1_000,
605+maxTokens: 100,
606+reasoning: false,
607+} as Model<Api>;
608+}
609+610+describe("resolveExplicitLiveModelCandidates", () => {
611+it("uses targeted registry lookup for explicit provider/model filters", () => {
612+const model = createGatewayLiveTestModel("xai", "grok-4.3");
613+const matcher = createLiveTargetMatcher({
614+providerFilter: new Set(["xai"]),
615+modelFilter: new Set(["xai/grok-4.3"]),
616+env: {},
617+});
618+const candidates = resolveExplicitLiveModelCandidates({
619+modelRegistry: {
620+find(provider, modelId) {
621+expect(provider).toBe("xai");
622+expect(modelId).toBe("grok-4.3");
623+return model;
624+},
625+getAll() {
626+throw new Error("explicit model lookup should not enumerate registry");
627+},
628+},
629+modelFilter: new Set(["xai/grok-4.3"]),
630+providerFilter: new Set(["xai"]),
631+targetMatcher: matcher,
632+});
633+634+expect(candidates).toEqual([model]);
635+});
636+637+it("falls back to enumeration for ambiguous model-only filters", () => {
638+const matcher = createLiveTargetMatcher({
639+providerFilter: null,
640+modelFilter: new Set(["grok-4.3"]),
641+env: {},
642+});
643+644+expect(
645+resolveExplicitLiveModelCandidates({
646+modelRegistry: {
647+find() {
648+throw new Error("ambiguous model-only lookup should not use direct find");
649+},
650+getAll() {
651+return [];
652+},
653+},
654+modelFilter: new Set(["grok-4.3"]),
655+providerFilter: null,
656+targetMatcher: matcher,
657+}),
658+).toBeNull();
659+});
660+});
661+662+describe("resolveGatewayLiveModelThinkingLevel", () => {
663+it("clamps requested thinking to levels supported by model metadata", () => {
664+expect(
665+resolveGatewayLiveModelThinkingLevel({
666+cfg: {},
667+model: {
668+ ...createGatewayLiveTestModel("xai", "grok-4.3"),
669+reasoning: true,
670+thinkingLevelMap: {
671+off: null,
672+minimal: null,
673+low: null,
674+medium: null,
675+high: null,
676+xhigh: null,
677+},
678+},
679+requestedLevel: "low",
680+}),
681+).toBe("off");
682+});
683+});
684+590685function isGoogleModelNotFoundText(text: string): boolean {
591686const trimmed = text.trim();
592687if (!trimmed) {
@@ -1281,6 +1376,101 @@ type GatewayModelSuiteParams = {
12811376providerOverrides?: Record<string, ModelProviderConfig>;
12821377};
128313781379+type LiveModelRegistry = {
1380+find(provider: string, modelId: string): Model<Api> | null | undefined;
1381+getAll(): Array<Model<Api>>;
1382+};
1383+1384+function parseExplicitLiveModelRef(
1385+raw: string,
1386+providerFilter: Set<string> | null,
1387+): { provider: string; modelId: string } | null {
1388+const trimmed = raw.trim();
1389+if (!trimmed) {
1390+return null;
1391+}
1392+const slash = trimmed.indexOf("/");
1393+if (slash !== -1) {
1394+const provider = normalizeProviderId(trimmed.slice(0, slash));
1395+const modelId = trimmed.slice(slash + 1).trim();
1396+return provider && modelId ? { provider, modelId } : null;
1397+}
1398+if (!providerFilter || providerFilter.size !== 1) {
1399+return null;
1400+}
1401+const [provider] = [...providerFilter];
1402+return provider ? { provider: normalizeProviderId(provider), modelId: trimmed } : null;
1403+}
1404+1405+function resolveExplicitLiveModelCandidates(params: {
1406+modelRegistry: LiveModelRegistry;
1407+modelFilter: Set<string> | null;
1408+providerFilter: Set<string> | null;
1409+targetMatcher: ReturnType<typeof createLiveTargetMatcher>;
1410+}): Array<Model<Api>> | null {
1411+if (!params.modelFilter || params.modelFilter.size === 0) {
1412+return null;
1413+}
1414+const candidates: Array<Model<Api>> = [];
1415+const seen = new Set<string>();
1416+for (const raw of params.modelFilter) {
1417+const ref = parseExplicitLiveModelRef(raw, params.providerFilter);
1418+if (!ref) {
1419+return null;
1420+}
1421+const model = params.modelRegistry.find(ref.provider, ref.modelId);
1422+if (!model) {
1423+return null;
1424+}
1425+if (
1426+!params.targetMatcher.matchesProvider(model.provider) ||
1427+!params.targetMatcher.matchesModel(model.provider, model.id)
1428+) {
1429+return null;
1430+}
1431+const key = `${normalizeProviderId(model.provider)}/${model.id.toLowerCase()}`;
1432+if (!seen.has(key)) {
1433+seen.add(key);
1434+candidates.push(model);
1435+}
1436+}
1437+return candidates;
1438+}
1439+1440+function resolveGatewayLiveModelThinkingLevel(params: {
1441+cfg: OpenClawConfig;
1442+model: Model<Api>;
1443+requestedLevel: string;
1444+}): string {
1445+const { model, requestedLevel } = params;
1446+const normalized = requestedLevel.trim() as ModelThinkingLevel;
1447+if (!["off", "minimal", "low", "medium", "high", "xhigh"].includes(normalized)) {
1448+return requestedLevel;
1449+}
1450+const profile = resolveProviderThinkingProfile({
1451+provider: model.provider,
1452+config: params.cfg,
1453+context: {
1454+provider: model.provider,
1455+modelId: model.id,
1456+reasoning: model.reasoning,
1457+},
1458+});
1459+if (profile) {
1460+const levelIds = profile.levels.map((level) => level.id);
1461+if (levelIds.includes(normalized)) {
1462+return normalized;
1463+}
1464+if (profile.defaultLevel) {
1465+return profile.defaultLevel;
1466+}
1467+if (levelIds.length === 1) {
1468+return levelIds[0] ?? requestedLevel;
1469+}
1470+}
1471+return clampThinkingLevel(model, normalized);
1472+}
1473+12841474function buildLiveGatewayConfig(params: {
12851475cfg: OpenClawConfig;
12861476candidates: Array<Model<Api>>;
@@ -1549,6 +1739,14 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
15491739for (const [index, model] of params.candidates.entries()) {
15501740const modelKey = `${model.provider}/${model.id}`;
15511741const progressLabel = `[${params.label}] ${index + 1}/${total} ${modelKey}`;
1742+const thinkingLevel = resolveGatewayLiveModelThinkingLevel({
1743+cfg: params.cfg,
1744+ model,
1745+requestedLevel: params.thinkingLevel,
1746+});
1747+if (thinkingLevel !== params.thinkingLevel) {
1748+logProgress(`${progressLabel}: thinking ${params.thinkingLevel} -> ${thinkingLevel}`);
1749+}
15521750// Use a separate session per model: live providers can finalize late after
15531751// skip/retry paths, and a reset on a reused key does not isolate those
15541752// delayed transcript writes from the next model probe.
@@ -1589,7 +1787,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
15891787 modelKey,
15901788message:
15911789"Explain in 2-3 sentences how the JavaScript event loop handles microtasks vs macrotasks. Must mention both words: microtask and macrotask.",
1592-thinkingLevel: params.thinkingLevel,
1790+ thinkingLevel,
15931791context: `${progressLabel}: prompt`,
15941792});
15951793if (!text) {
@@ -1601,7 +1799,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
16011799 modelKey,
16021800message:
16031801"Explain in 2-3 sentences how the JavaScript event loop handles microtasks vs macrotasks. Must mention both words: microtask and macrotask.",
1604-thinkingLevel: params.thinkingLevel,
1802+ thinkingLevel,
16051803context: `${progressLabel}: prompt-retry`,
16061804});
16071805}
@@ -1650,7 +1848,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
16501848 modelKey,
16511849message:
16521850"Answer in exactly two short sentences. Include the exact lowercase words microtask and macrotask. No bullets.",
1653-thinkingLevel: params.thinkingLevel,
1851+ thinkingLevel,
16541852context: `${progressLabel}: prompt-keyword-retry`,
16551853});
16561854if (retryText) {
@@ -1697,7 +1895,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
16971895 : "OpenClaw live tool probe (local, safe): " +
16981896`use the tool named \`read\` (or \`Read\`) with JSON arguments {"path":"${toolProbePath}"}. ` +
16991897"Then reply with the two nonce values you read (include both).",
1700-thinkingLevel: params.thinkingLevel,
1898+ thinkingLevel,
17011899context: `${progressLabel}: tool-read`,
17021900});
17031901if (
@@ -1768,7 +1966,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
17681966`mkdir -p "${tempDir}" && printf '%s' '${nonceC}' > "${toolWritePath}". ` +
17691967`Then use the tool named \`read\` (or \`Read\`) with JSON arguments {"path":"${toolWritePath}"}. ` +
17701968"Finally reply including the nonce text you read back.",
1771-thinkingLevel: params.thinkingLevel,
1969+ thinkingLevel,
17721970context: `${progressLabel}: tool-exec`,
17731971});
17741972if (
@@ -1836,7 +2034,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
18362034content: imageBase64,
18372035},
18382036],
1839-thinkingLevel: params.thinkingLevel,
2037+ thinkingLevel,
18402038context: `${progressLabel}: image`,
18412039});
18422040if (
@@ -1883,7 +2081,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
18832081idempotencyKey: `idem-${runId2}-1`,
18842082 modelKey,
18852083message: `Call the tool named \`read\` (or \`Read\`) on "${toolProbePath}". Do not write any other text.`,
1886-thinkingLevel: params.thinkingLevel,
2084+ thinkingLevel,
18872085context: `${progressLabel}: tool-only-regression-first`,
18882086});
18892087assertNoReasoningTags({
@@ -1899,7 +2097,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
18992097idempotencyKey: `idem-${runId2}-2`,
19002098 modelKey,
19012099message: `Now answer: what are the values of nonceA and nonceB in "${toolProbePath}"? Reply with exactly: ${nonceA} ${nonceB}.`,
1902-thinkingLevel: params.thinkingLevel,
2100+ thinkingLevel,
19032101context: `${progressLabel}: tool-only-regression-second`,
19042102});
19052103assertNoReasoningTags({
@@ -1919,7 +2117,7 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
19192117 sessionKey,
19202118 modelKey,
19212119label: progressLabel,
1922-thinkingLevel: params.thinkingLevel,
2120+ thinkingLevel,
19232121});
19242122}
19252123return "done";
@@ -2171,7 +2369,6 @@ describeLive("gateway live (dev agent, profile keys)", () => {
21712369const agentDir = resolveDefaultAgentDir(cfg);
21722370const authStorage = discoverAuthStorage(agentDir);
21732371const modelRegistry = discoverModels(authStorage, agentDir);
2174-const all = modelRegistry.getAll();
2175237221762373const rawModels = process.env.OPENCLAW_LIVE_GATEWAY_MODELS?.trim();
21772374const useModern = !rawModels || rawModels === "modern" || rawModels === "all";
@@ -2184,18 +2381,29 @@ describeLive("gateway live (dev agent, profile keys)", () => {
21842381config: cfg,
21852382env: process.env,
21862383});
2187-const wanted = filter
2188- ? all.filter((m) => targetMatcher.matchesModel(m.provider, m.id))
2189- : all.filter(
2190-(m) =>
2191-!shouldExcludeProviderFromDefaultHighSignalLiveSweep({
2192-provider: m.provider,
2193-useExplicitModels: useExplicit,
2194-providerFilter: PROVIDERS,
2195-config: cfg,
2196-env: process.env,
2197-}) && isHighSignalLiveModelRef({ provider: m.provider, id: m.id }),
2198-);
2384+let wanted = useExplicit
2385+ ? resolveExplicitLiveModelCandidates({
2386+ modelRegistry,
2387+modelFilter: filter,
2388+providerFilter: PROVIDERS,
2389+ targetMatcher,
2390+})
2391+ : null;
2392+if (!wanted) {
2393+const all = modelRegistry.getAll();
2394+wanted = filter
2395+ ? all.filter((m) => targetMatcher.matchesModel(m.provider, m.id))
2396+ : all.filter(
2397+(m) =>
2398+!shouldExcludeProviderFromDefaultHighSignalLiveSweep({
2399+provider: m.provider,
2400+useExplicitModels: useExplicit,
2401+providerFilter: PROVIDERS,
2402+config: cfg,
2403+env: process.env,
2404+}) && isHighSignalLiveModelRef({ provider: m.provider, id: m.id }),
2405+);
2406+}
2199240722002408const candidates: Array<Model<Api>> = [];
22012409const skipped: Array<{ model: string; error: string }> = [];
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。