mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
fix: align native controls with scoped model params
This commit is contained in:
@@ -1091,18 +1091,18 @@ describe("openrouter provider hooks", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("prefers the exact OpenRouter provider key over a differently cased key", async () => {
|
||||
it("prefers the exact OpenRouter provider key regardless of config key order", async () => {
|
||||
const provider = await registerSingleProviderPlugin(openrouterPlugin);
|
||||
const patch = provider.extraParamsForTransport?.({
|
||||
config: {
|
||||
models: {
|
||||
providers: {
|
||||
OpenRouter: {
|
||||
params: { provider: { order: ["anthropic"], allow_fallbacks: false } },
|
||||
},
|
||||
openrouter: {
|
||||
params: { provider: { order: ["openai"], allow_fallbacks: false } },
|
||||
},
|
||||
OpenRouter: {
|
||||
params: { provider: { order: ["anthropic"], allow_fallbacks: false } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -76,12 +76,16 @@ function resolveOpenRouterProviderConfigParams(
|
||||
|
||||
const providers = Object.entries(ctx.config?.models?.providers ?? {});
|
||||
// Preserve routing split across normalized duplicates; merge nested params
|
||||
// field-wise while allowing later scalar settings to override earlier ones.
|
||||
// field-wise, then apply exact-key rows so aliases cannot override them.
|
||||
const matchedProviders = providers.filter(
|
||||
([provider]) => normalizeProviderId(provider) === normalizedProvider,
|
||||
);
|
||||
const prioritizedProviders = [
|
||||
...matchedProviders.filter(([provider]) => provider.trim() !== requestedProvider),
|
||||
...matchedProviders.filter(([provider]) => provider.trim() === requestedProvider),
|
||||
];
|
||||
let matchedParams: Record<string, unknown> | undefined;
|
||||
for (const [provider, config] of providers) {
|
||||
if (normalizeProviderId(provider) !== normalizedProvider) {
|
||||
continue;
|
||||
}
|
||||
for (const [, config] of prioritizedProviders) {
|
||||
const params = readRecord(config.params);
|
||||
if (params) {
|
||||
matchedParams = mergeOpenRouterProviderConfigParams(matchedParams, params);
|
||||
|
||||
@@ -119,6 +119,43 @@ describe("resolveFastModeState", () => {
|
||||
expect(state.source).toBe("config");
|
||||
});
|
||||
|
||||
it("uses per-agent model fast params at the narrowest config precedence", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
params: { fast_mode: false, fast_seconds: 90 },
|
||||
models: {
|
||||
"openai/gpt-5.5": { params: { fastMode: true, fastAutoOnSeconds: 60 } },
|
||||
},
|
||||
},
|
||||
entries: {
|
||||
audit: {
|
||||
params: { fastMode: false, fastAutoOnSeconds: 30 },
|
||||
models: {
|
||||
"openai/gpt-5.5": {
|
||||
params: { fast_mode: "auto", fast_seconds: 15 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const state = resolveFastModeState({
|
||||
cfg,
|
||||
provider: "openai",
|
||||
model: "gpt-5.5",
|
||||
agentId: "audit",
|
||||
});
|
||||
|
||||
expect(state).toMatchObject({
|
||||
mode: "auto",
|
||||
enabled: true,
|
||||
source: "config",
|
||||
fastAutoOnSeconds: 15,
|
||||
});
|
||||
});
|
||||
|
||||
it("formats auto mode with the default threshold", () => {
|
||||
expect(formatFastModeAutoLabel()).toBe("auto (60 sec)");
|
||||
expect(formatFastModeStatusValue({ mode: "auto" })).toBe("auto (60 sec)");
|
||||
|
||||
+50
-7
@@ -6,11 +6,12 @@ import { normalizeFastMode } from "../auto-reply/thinking.shared.js";
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import {
|
||||
DEFAULT_FAST_MODE_AUTO_ON_SECONDS,
|
||||
type FastModeSource,
|
||||
resolveFastModeModelAutoOnSeconds,
|
||||
resolveFastModeModelParams,
|
||||
} from "../shared/fast-mode.js";
|
||||
import { resolveAgentConfig } from "./agent-scope.js";
|
||||
import { resolveModelExtraParamSources } from "./model-extra-params.js";
|
||||
|
||||
export {
|
||||
DEFAULT_FAST_MODE_AUTO_ON_SECONDS,
|
||||
@@ -33,13 +34,51 @@ type FastModeState = {
|
||||
fastAutoOnSeconds: number;
|
||||
};
|
||||
|
||||
function resolveConfiguredFastModeRaw(params: {
|
||||
function resolveConfiguredFastModeParamSources(params: {
|
||||
cfg: OpenClawConfig | undefined;
|
||||
provider: string;
|
||||
model: string;
|
||||
}): unknown {
|
||||
const modelParams = resolveFastModeModelParams(params);
|
||||
return modelParams?.fastMode ?? modelParams?.fast_mode;
|
||||
agentId?: string;
|
||||
}): Array<Record<string, unknown> | undefined> {
|
||||
const sources = resolveModelExtraParamSources({
|
||||
config: params.cfg,
|
||||
provider: params.provider,
|
||||
modelId: params.model,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
return [
|
||||
sources.agentModelParams,
|
||||
sources.agentEntryParams,
|
||||
resolveFastModeModelParams(params),
|
||||
sources.defaultParams,
|
||||
];
|
||||
}
|
||||
|
||||
function resolveConfiguredFastModeValue(
|
||||
sources: Array<Record<string, unknown> | undefined>,
|
||||
keys: readonly string[],
|
||||
accepts?: (value: unknown) => boolean,
|
||||
): unknown {
|
||||
for (const source of sources) {
|
||||
for (const key of keys) {
|
||||
const value = source?.[key];
|
||||
if (source && Object.hasOwn(source, key) && (!accepts || accepts(value))) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveConfiguredFastModeAutoOnSeconds(
|
||||
sources: Array<Record<string, unknown> | undefined>,
|
||||
): number {
|
||||
const value = resolveConfiguredFastModeValue(
|
||||
sources,
|
||||
["fastAutoOnSeconds", "fast_auto_on_seconds", "fastSeconds", "fast_seconds"],
|
||||
(candidate) => typeof candidate === "number" && Number.isInteger(candidate) && candidate > 0,
|
||||
);
|
||||
return typeof value === "number" ? value : DEFAULT_FAST_MODE_AUTO_ON_SECONDS;
|
||||
}
|
||||
|
||||
/** Resolve the effective fast-mode setting and its source. */
|
||||
@@ -50,7 +89,8 @@ export function resolveFastModeState(params: {
|
||||
agentId?: string;
|
||||
sessionEntry?: Pick<SessionEntry, "fastMode"> | undefined;
|
||||
}): FastModeState {
|
||||
const fastAutoOnSeconds = resolveFastModeModelAutoOnSeconds(params);
|
||||
const configuredParamSources = resolveConfiguredFastModeParamSources(params);
|
||||
const fastAutoOnSeconds = resolveConfiguredFastModeAutoOnSeconds(configuredParamSources);
|
||||
const sessionOverride = normalizeFastMode(params.sessionEntry?.fastMode);
|
||||
if (sessionOverride !== undefined) {
|
||||
return {
|
||||
@@ -75,7 +115,10 @@ export function resolveFastModeState(params: {
|
||||
};
|
||||
}
|
||||
|
||||
const configuredRaw = resolveConfiguredFastModeRaw(params);
|
||||
const configuredRaw = resolveConfiguredFastModeValue(configuredParamSources, [
|
||||
"fastMode",
|
||||
"fast_mode",
|
||||
]);
|
||||
const configured = normalizeFastMode(configuredRaw as string | boolean | null | undefined);
|
||||
if (configured !== undefined) {
|
||||
return {
|
||||
|
||||
@@ -8,6 +8,8 @@ type ModelExtraParamSources = {
|
||||
defaultParams?: Record<string, unknown>;
|
||||
modelParams?: Record<string, unknown>;
|
||||
agentParams?: Record<string, unknown>;
|
||||
agentEntryParams?: Record<string, unknown>;
|
||||
agentModelParams?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const FAST_MODE_CUTOFF_MODEL_PARAM_KEYS = new Set([
|
||||
@@ -62,6 +64,7 @@ export function resolveModelExtraParamSources(params: {
|
||||
: undefined;
|
||||
const agentConfig =
|
||||
params.agentId && params.config ? resolveAgentConfig(params.config, params.agentId) : undefined;
|
||||
const agentEntryParams = agentConfig?.params;
|
||||
const agentModelParams = canonicalKey
|
||||
? (agentConfig?.models?.[canonicalKey]?.params ??
|
||||
(legacyKey ? agentConfig?.models?.[legacyKey]?.params : undefined))
|
||||
@@ -69,9 +72,28 @@ export function resolveModelExtraParamSources(params: {
|
||||
// Model-specific agent settings are narrower than agent-wide settings and
|
||||
// must stay in the same precedence source for transport alias normalization.
|
||||
const agentParams = agentModelParams
|
||||
? { ...agentConfig?.params, ...agentModelParams }
|
||||
: agentConfig?.params;
|
||||
return { defaultParams, modelParams, agentParams };
|
||||
? { ...agentEntryParams, ...agentModelParams }
|
||||
: agentEntryParams;
|
||||
return { defaultParams, modelParams, agentParams, agentEntryParams, agentModelParams };
|
||||
}
|
||||
|
||||
/** Resolves one authored parameter across the canonical config precedence. */
|
||||
export function resolveModelExtraParamValue(
|
||||
params: Parameters<typeof resolveModelExtraParamSources>[0],
|
||||
key: string,
|
||||
): unknown {
|
||||
const sources = resolveModelExtraParamSources(params);
|
||||
for (const source of [
|
||||
sources.agentModelParams,
|
||||
sources.agentEntryParams,
|
||||
sources.modelParams,
|
||||
sources.defaultParams,
|
||||
]) {
|
||||
if (source && Object.hasOwn(source, key)) {
|
||||
return source[key];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Returns whether embedded OpenClaw would apply authored provider request parameters. */
|
||||
|
||||
@@ -621,6 +621,42 @@ describe("createModelSelectionState catalog loading", () => {
|
||||
await expect(state.resolveDefaultThinkingLevel()).resolves.toBe("minimal");
|
||||
});
|
||||
|
||||
it("uses per-agent model thinking at the narrowest config precedence", async () => {
|
||||
vi.mocked(loadModelCatalogLocal).mockClear();
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
params: { thinking: "low" },
|
||||
models: {
|
||||
"openai/gpt-5.4": { params: { thinking: "medium" } },
|
||||
},
|
||||
},
|
||||
entries: {
|
||||
audit: {
|
||||
params: { thinking: "high" },
|
||||
models: {
|
||||
"openai/gpt-5.4": { params: { thinking: "xhigh" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const state = await createModelSelectionState({
|
||||
cfg,
|
||||
agentId: "audit",
|
||||
agentCfg: cfg.agents?.defaults,
|
||||
defaultProvider: "openai",
|
||||
defaultModel: "gpt-5.4",
|
||||
provider: "openai",
|
||||
model: "gpt-5.4",
|
||||
hasModelDirective: false,
|
||||
});
|
||||
|
||||
await expect(state.resolveDefaultThinkingLevel()).resolves.toBe("xhigh");
|
||||
expect(loadModelCatalogLocal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("loads the full catalog for explicit model directives", async () => {
|
||||
vi.mocked(loadModelCatalogLocal).mockClear();
|
||||
const cfg = {
|
||||
|
||||
@@ -8,11 +8,11 @@ import { clearSessionAuthProfileOverride } from "../../agents/auth-profiles/sess
|
||||
import { resolveAgentHarnessPolicy } from "../../agents/harness/policy.js";
|
||||
import type { ModelCatalogEntry } from "../../agents/model-catalog.js";
|
||||
import type { ModelCatalogSnapshot } from "../../agents/model-catalog.types.js";
|
||||
import { resolveModelExtraParamValue } from "../../agents/model-extra-params.js";
|
||||
import type { ModelFallbackRouteResolution } from "../../agents/model-fallback.types.js";
|
||||
import {
|
||||
type ModelAliasIndex,
|
||||
buildConfiguredModelCatalog,
|
||||
legacyModelKey,
|
||||
modelKey,
|
||||
normalizeProviderId,
|
||||
resolveModelAliasFromPair,
|
||||
@@ -629,14 +629,16 @@ export async function createModelSelectionState(params: {
|
||||
defaultThinkingLevels.set(cacheKey, agentThinkingDefault);
|
||||
return agentThinkingDefault;
|
||||
}
|
||||
const configuredModels = cfg.agents?.defaults?.models;
|
||||
const canonicalKey = modelKey(selectedProvider, selectedModel);
|
||||
const legacyKey = legacyModelKey(selectedProvider, selectedModel);
|
||||
const configuredModelThinkingDefault =
|
||||
configuredModels?.[canonicalKey]?.params?.thinking ??
|
||||
(legacyKey ? configuredModels?.[legacyKey]?.params?.thinking : undefined);
|
||||
const resolvedConfiguredModelThinkingDefault = resolveConfiguredModelThinkingDefault(
|
||||
configuredModelThinkingDefault,
|
||||
resolveModelExtraParamValue(
|
||||
{
|
||||
config: cfg,
|
||||
provider: selectedProvider,
|
||||
modelId: selectedModel,
|
||||
agentId: params.agentId,
|
||||
},
|
||||
"thinking",
|
||||
),
|
||||
);
|
||||
if (resolvedConfiguredModelThinkingDefault) {
|
||||
defaultThinkingLevels.set(cacheKey, resolvedConfiguredModelThinkingDefault);
|
||||
@@ -707,12 +709,10 @@ export async function createModelSelectionState(params: {
|
||||
provider,
|
||||
model,
|
||||
});
|
||||
const configuredModels = cfg.agents?.defaults?.models;
|
||||
const canonicalKey = modelKey(provider, model);
|
||||
const legacyKey = legacyModelKey(provider, model);
|
||||
const configuredModelThinkingDefault =
|
||||
configuredModels?.[canonicalKey]?.params?.thinking ??
|
||||
(legacyKey ? configuredModels?.[legacyKey]?.params?.thinking : undefined);
|
||||
const configuredModelThinkingDefault = resolveModelExtraParamValue(
|
||||
{ config: cfg, provider, modelId: model, agentId: params.agentId },
|
||||
"thinking",
|
||||
);
|
||||
const hasConfiguredThinkingDefault =
|
||||
agentEntry?.thinkingDefault !== undefined ||
|
||||
resolveConfiguredModelThinkingDefault(configuredModelThinkingDefault) !== undefined ||
|
||||
|
||||
Reference in New Issue
Block a user