refactor: consolidate provider parameter precedence

This commit is contained in:
Dallin Romney
2026-08-13 21:53:59 +08:00
parent ceedc6f914
commit 1a28bd4341
23 changed files with 334 additions and 645 deletions
+1 -1
View File
@@ -433,7 +433,7 @@ date context. Falls back to the host timezone.
- For direct Anthropic models using API-key auth, set `params.anthropicServerCompaction: true` to enable server-side compaction. Use `params.anthropicCompactThreshold` to override the input-token trigger; the default is `max(50000, floor(contextWindow * 0.7))`, and lower configured values clamp to `50000`. OAuth/subscription and non-direct endpoints are excluded. See [Anthropic server-side compaction](/providers/anthropic#advanced-configuration).
- For store-capable direct OpenAI Responses models, server-side compaction is enabled automatically and the same effective threshold delays local preflight compaction. Use `params.responsesServerCompaction: false` to stop injecting `context_management`, or `params.responsesCompactThreshold` to override the default of 70% of the resolved context window (80,000 when unavailable). ChatGPT OAuth, custom proxies, and routes with `compat.supportsStore: false` do not enable this path. See [OpenAI server-side compaction](/providers/openai#advanced-configuration).
- `params`: global default provider parameters applied to all models. Set at `agents.defaults.params` (e.g. `{ cacheRetention: "long" }`).
- `params` merge precedence (config): `agents.defaults.params` (global base) is overridden by `agents.defaults.models["provider/model"].params` (per-model), then `agents.entries.*.params` (matching agent id), and finally `agents.entries.*.models["provider/model"].params` (matching agent and model). Later scopes override earlier scopes by key. See [Prompt Caching](/reference/prompt-caching) for details.
- `params` merge precedence (config): `agents.defaults.params` (global base) is overridden by `agents.defaults.models["provider/model"].params` (per-model), then `agents.entries.*.params` (matching agent id), and finally `agents.entries.*.models["provider/model"].params` (matching agent and model). Later scopes override earlier scopes, including supported camelCase/snake_case alias spellings. See [Prompt Caching](/reference/prompt-caching) for details.
- `models.providers.openrouter.params.provider`: OpenRouter-wide default provider-routing policy. OpenClaw forwards this to OpenRouter's request `provider` object; per-model `agents.defaults.models["openrouter/<model>"].params.provider` and agent params override by key. See [OpenRouter provider routing](/providers/openrouter#advanced-configuration).
- `params.extra_body`/`params.extraBody`: advanced pass-through JSON merged into `api: "openai-completions"` request bodies for OpenAI-compatible proxies. If it collides with generated request keys, the extra body wins; non-native completions routes still strip OpenAI-only `store` afterward.
- `params.chat_template_kwargs`: vLLM/OpenAI-compatible chat-template arguments merged into top-level `api: "openai-completions"` request bodies. For `vllm/nemotron-3-*` with thinking off, the bundled vLLM plugin automatically sends `enable_thinking: false` and `force_nonempty_content: true`; explicit `chat_template_kwargs` override generated defaults, and `extra_body.chat_template_kwargs` still has final precedence. Configured vLLM Qwen and Nemotron thinking models expose binary `/think` choices (`off`, `on`) instead of the multi-level effort ladder.
@@ -143,6 +143,7 @@ export function buildDiscordCommandOptions(params: {
cfg: currentCfg,
provider: context?.provider,
model: context?.model,
agentId: context?.agentId,
agentRuntime: context?.agentRuntime,
...(choiceCatalog?.length ? { catalog: choiceCatalog } : {}),
});
@@ -525,6 +525,7 @@ async function dispatchDiscordCommandInteraction(params: {
cfg,
provider: menuModelContext?.provider,
model: menuModelContext?.model,
agentId: menuModelContext?.agentId,
agentRuntime: menuModelContext?.agentRuntime,
...(menuModelCatalog?.length ? { catalog: menuModelCatalog } : {}),
});
-75
View File
@@ -539,81 +539,6 @@ describe("buildOpenAIProvider", () => {
},
);
it.each([
{
name: "the official OpenAI API",
baseUrl: OPENAI_API_BASE_URL,
expectedFetches: 1,
},
{
name: "a custom OpenAI-compatible endpoint",
baseUrl: "https://example-proxy.invalid/v1",
expectedFetches: 0,
},
])(
"keeps a selected $name API key when an OAuth profile is also available",
async ({ baseUrl, expectedFetches }) => {
mocks.resolveApiKeyForProvider.mockResolvedValue({
mode: "oauth",
apiKey: "synthetic-oauth-token",
source: "profile:openai:chatgpt",
profileId: "openai:chatgpt",
});
mocks.resolveProviderAuthProfileMetadata.mockReturnValue({
profileId: "openai:chatgpt",
accountId: "synthetic-oauth-account",
});
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
Response.json({
data: [{ id: "gpt-5.5", object: "model" }],
}),
);
try {
const result = await buildOpenAIProvider().catalog?.run({
resolveProviderAuth: () => ({
mode: "api_key",
apiKey: "synthetic-platform-key",
discoveryApiKey: "synthetic-platform-discovery-key",
source: "env",
}),
config: {
auth: {
profiles: {
"openai:chatgpt": { provider: "openai", mode: "oauth" },
},
},
models: { providers: { openai: { baseUrl, models: [] } } },
},
agentDir: "/tmp/openai-agent",
workspaceDir: "/tmp/openai-workspace",
} as never);
if (!result || "provider" in result) {
throw new Error("expected OpenAI API-key provider catalog");
}
expect(result.providers.openai).toMatchObject({
api: "openai-responses",
apiKey: "synthetic-platform-key",
baseUrl,
});
expect(mocks.resolveApiKeyForProvider).not.toHaveBeenCalled();
expect(mocks.resolveProviderAuthProfileMetadata).not.toHaveBeenCalled();
expect(fetchSpy).toHaveBeenCalledTimes(expectedFetches);
if (expectedFetches > 0) {
const headers = fetchSpy.mock.calls[0]?.[1]?.headers;
expect(headers).toBeInstanceOf(Headers);
if (!(headers instanceof Headers)) {
throw new Error("expected OpenAI discovery request headers");
}
expect(headers.get("Authorization")).toBe("Bearer synthetic-platform-discovery-key");
}
} finally {
fetchSpy.mockRestore();
}
},
);
it("falls back to direct API-key catalog discovery when OAuth resolution fails", async () => {
mocks.resolveApiKeyForProvider.mockRejectedValue(new Error("expired oauth profile"));
const provider = buildOpenAIProvider();
+17 -20
View File
@@ -950,6 +950,14 @@ export function buildOpenAIProvider(): ProviderPlugin {
return null;
}
const auth = ctx.resolveProviderAuth(PROVIDER_ID);
const selectedApiKey =
auth.mode === "api_key" && auth.apiKey
? {
apiKey: auth.apiKey,
discoveryApiKey: auth.discoveryApiKey,
profileId: auth.profileId,
}
: undefined;
// Catalog auth is already selected. Re-resolving an explicit API key
// would let an unrelated OAuth profile replace its account and models.
if (auth.mode !== "api_key") {
@@ -994,29 +1002,18 @@ export function buildOpenAIProvider(): ProviderPlugin {
// auth can still publish the standard OpenAI catalog.
}
}
if (auth.mode === "api_key" && auth.apiKey) {
const catalog = scopeOpenAICatalogOutcome(
await buildOpenAILiveProviderConfig({
apiKey: auth.apiKey,
baseUrl: resolveOpenAICatalogBaseUrl(ctx),
discoveryApiKey: auth.discoveryApiKey,
}),
auth.profileId,
);
return {
providers: { [PROVIDER_ID]: catalog.provider },
...(catalog.outcome ? { outcomes: [catalog.outcome] } : {}),
};
}
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID);
const apiKey = selectedApiKey ?? ctx.resolveProviderApiKey(PROVIDER_ID);
if (!apiKey.apiKey) {
return null;
}
const catalog = await buildOpenAILiveProviderConfig({
apiKey: apiKey.apiKey,
baseUrl: resolveOpenAICatalogBaseUrl(ctx),
discoveryApiKey: apiKey.discoveryApiKey,
});
const catalog = scopeOpenAICatalogOutcome(
await buildOpenAILiveProviderConfig({
apiKey: apiKey.apiKey,
baseUrl: resolveOpenAICatalogBaseUrl(ctx),
discoveryApiKey: apiKey.discoveryApiKey,
}),
selectedApiKey?.profileId,
);
return {
providers: { [PROVIDER_ID]: catalog.provider },
...(catalog.outcome ? { outcomes: [catalog.outcome] } : {}),
@@ -76,7 +76,6 @@ describe("OpenAI realtime voice provider routing", () => {
{ encoding: "g711_ulaw", sampleRateHz: 8000, channels: 1 },
{ encoding: "pcm16", sampleRateHz: 24000, channels: 1 },
],
supportsActivationNameGating: true,
supportsBrowserSession: true,
supportsBargeIn: true,
handlesInputAudioBargeIn: true,
+18 -175
View File
@@ -515,6 +515,7 @@ describe("openrouter provider hooks", () => {
provider: "openrouter",
modelId: "openrouter/fusion",
promptMode: "full",
agentId: "analyst",
config: {
agents: {
defaults: {
@@ -525,7 +526,7 @@ describe("openrouter provider hooks", () => {
plugins: [
{
id: "fusion",
analysis_models: ["deepseek/deepseek-v4-pro"],
analysis_models: ["default/model"],
},
],
},
@@ -533,33 +534,6 @@ describe("openrouter provider hooks", () => {
},
},
},
},
},
} as never);
expect(contribution?.dynamicSuffix).toContain("Analysis models: deepseek/deepseek-v4-pro.");
});
it("describes Fusion config from the selected agent model", async () => {
const provider = await registerSingleProviderPlugin(openrouterPlugin);
const contribution = provider.resolveSystemPromptContribution?.({
provider: "openrouter",
modelId: "openrouter/fusion",
promptMode: "full",
agentId: "analyst",
config: {
agents: {
defaults: {
models: {
"openrouter/fusion": {
params: {
extraBody: {
plugins: [{ id: "fusion", analysis_models: ["default/model"] }],
},
},
},
},
},
entries: {
analyst: {
models: {
@@ -1050,16 +1024,26 @@ describe("openrouter provider hooks", () => {
sort: "price",
data_collection: "deny",
},
frequencyPenalty: 0.3,
},
},
" OpenRouter ": {
params: {
provider: {
sort: "throughput",
allow_fallbacks: false,
},
frequencyPenalty: 0.1,
topK: 40,
},
},
},
},
},
provider: "openrouter",
provider: " openrouter ",
modelId: "openai/gpt-5.4",
extraParams: {
provider: {
sort: "latency",
require_parameters: true,
},
temperature: 0.2,
@@ -1081,158 +1065,17 @@ describe("openrouter provider hooks", () => {
expect(patch?.responseCache).toBe(true);
expect(patch?.temperature).toBe(0.2);
expect(patch?.frequencyPenalty).toBe(0.3);
expect(patch?.topK).toBe(40);
expect(patch?.provider).toEqual({
sort: "latency",
sort: "price",
data_collection: "deny",
allow_fallbacks: false,
order: ["openai"],
require_parameters: true,
});
});
it.each([
{ configuredProvider: "openrouter", runtimeProvider: "openrouter" },
{ configuredProvider: "OpenRouter", runtimeProvider: "openrouter" },
{ configuredProvider: " openrouter ", runtimeProvider: "openrouter" },
{ configuredProvider: "openrouter", runtimeProvider: "OpenRouter" },
{ configuredProvider: "openrouter", runtimeProvider: " openrouter " },
])(
"preserves configured OpenRouter routing for $configuredProvider and $runtimeProvider",
async ({ configuredProvider, runtimeProvider }) => {
const provider = await registerSingleProviderPlugin(openrouterPlugin);
const patch = provider.extraParamsForTransport?.({
config: {
models: {
providers: {
[configuredProvider]: {
params: {
provider: {
order: ["anthropic"],
allow_fallbacks: false,
},
temperature: 0.25,
},
},
},
},
},
provider: runtimeProvider,
modelId: "openai/gpt-5.4",
extraParams: { topP: 0.7 },
transport: "sse",
} as never)?.patch;
expect(patch).toEqual({
provider: {
order: ["anthropic"],
allow_fallbacks: false,
},
temperature: 0.25,
topP: 0.7,
});
},
);
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: ["openai"], allow_fallbacks: false } },
},
OpenRouter: {
params: { provider: { order: ["anthropic"], allow_fallbacks: false } },
},
},
},
},
provider: "openrouter",
modelId: "openai/gpt-5.4",
extraParams: {},
transport: "sse",
} as never)?.patch;
expect(patch?.provider).toEqual({ order: ["openai"], allow_fallbacks: false });
});
it("preserves the later routing config for trimmed duplicate OpenRouter keys", 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 },
temperature: 0.25,
},
},
},
},
},
provider: "openrouter",
modelId: "openai/gpt-5.4",
extraParams: {},
transport: "sse",
} as never)?.patch;
expect(patch).toEqual({
provider: { order: ["openai"], allow_fallbacks: false },
temperature: 0.25,
});
});
it("merges routing split across case- and whitespace-equivalent OpenRouter keys", async () => {
const provider = await registerSingleProviderPlugin(openrouterPlugin);
const patch = provider.extraParamsForTransport?.({
config: {
models: {
providers: {
" OpenRouter ": {
params: {
provider: {
order: ["anthropic"],
allow_fallbacks: false,
},
responseCache: true,
temperature: 0.1,
},
},
openrouter: {
params: {
provider: {
sort: "price",
require_parameters: true,
},
temperature: 0.25,
},
},
},
},
},
provider: "openrouter",
modelId: "openai/gpt-5.4",
extraParams: { topP: 0.7 },
transport: "sse",
} as never)?.patch;
expect(patch).toEqual({
provider: {
order: ["anthropic"],
allow_fallbacks: false,
sort: "price",
require_parameters: true,
},
responseCache: true,
temperature: 0.25,
topP: 0.7,
});
});
it("does not inject OpenRouter reasoning for Hunter Alpha", async () => {
const capturedPayload = await captureOpenRouterWrappedPayload({
modelId: "openrouter/hunter-alpha",
+2 -17
View File
@@ -1,4 +1,5 @@
// Openrouter provider module implements model/runtime integration.
import { mergeDeep } from "openclaw/plugin-sdk/plugin-config-runtime";
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
type OpenRouterProviderConfig = {
@@ -49,22 +50,6 @@ function readRecord(value: unknown): Record<string, unknown> | undefined {
return Object.keys(sanitized).length > 0 ? sanitized : undefined;
}
function mergeOpenRouterProviderConfigParams(
previous: Record<string, unknown> | undefined,
next: Record<string, unknown>,
): Record<string, unknown> {
const merged = { ...previous };
for (const [key, value] of Object.entries(next)) {
const previousRecord = readRecord(merged[key]);
const nextRecord = readRecord(value);
merged[key] =
previousRecord && nextRecord
? mergeOpenRouterProviderConfigParams(previousRecord, nextRecord)
: value;
}
return merged;
}
function resolveOpenRouterProviderConfigParams(
ctx: OpenRouterExtraParamsContext,
): Record<string, unknown> | undefined {
@@ -88,7 +73,7 @@ function resolveOpenRouterProviderConfigParams(
for (const [, config] of prioritizedProviders) {
const params = readRecord(config.params);
if (params) {
matchedParams = mergeOpenRouterProviderConfigParams(matchedParams, params);
matchedParams = mergeDeep(matchedParams ?? {}, params) as Record<string, unknown>;
}
}
return matchedParams;
+1
View File
@@ -696,6 +696,7 @@ export async function registerSlackMonitorSlashCommands(params: {
command: commandDefinition,
args: commandArgs,
cfg,
agentId: menuRoute?.agentId,
...menuModelContext,
...(menuModelCatalog?.length ? { catalog: menuModelCatalog } : {}),
});
@@ -321,6 +321,7 @@ export async function executeTelegramBuiltinCommand(
command: commandDefinition,
args: commandArgs,
cfg: dispatch.runtimeCfg,
agentId: dispatch.route.agentId,
...menuModelContext,
...(menuModelCatalog?.length ? { catalog: menuModelCatalog } : {}),
})
@@ -2,29 +2,6 @@
import { describe, expect, it } from "vitest";
import { resolveExtraParams } from "./embedded-agent-runner/extra-params.js";
const AGENT_MODEL_PARAM_CASES = [
{
provider: "openai",
modelId: "gpt-5.6-luna",
params: { temperature: 0.1, topP: 0.2, serviceTier: "priority", transport: "websocket" },
},
{
provider: "anthropic",
modelId: "claude-sonnet-4-6",
params: { temperature: 0.2, topP: 0.3, maxTokens: 321, cacheRetention: "short" },
},
{
provider: "google",
modelId: "gemini-2.5-pro",
params: { temperature: 0.3, topP: 0.4, cachedContent: "cachedContents/agent-model-proof" },
},
{
provider: "google-vertex",
modelId: "gemini-2.5-pro",
params: { temperature: 0.4, topP: 0.5, maxTokens: 456 },
},
];
describe("resolveExtraParams", () => {
it("returns undefined with no model config", () => {
const result = resolveExtraParams({
@@ -162,108 +139,72 @@ describe("resolveExtraParams", () => {
});
});
it.each(AGENT_MODEL_PARAM_CASES)(
"applies canonical agent-specific model params for $provider/$modelId",
({ provider, modelId, params }) => {
const modelRef = `${provider}/${modelId}`;
const result = resolveExtraParams({
cfg: {
agents: {
entries: {
audit: {
models: { [modelRef]: { params } },
},
},
},
},
provider,
modelId,
agentId: "audit",
});
expect(result).toEqual(expect.objectContaining(params));
},
);
it.each(AGENT_MODEL_PARAM_CASES)(
"applies the narrowest agent-specific model precedence for $provider/$modelId",
({ provider, modelId, params }) => {
const modelRef = `${provider}/${modelId}`;
const result = resolveExtraParams({
cfg: {
agents: {
defaults: {
params: { temperature: 0.9, topP: 0.9, cacheRetention: "long" },
models: {
[modelRef]: {
params: { temperature: 0.8, topP: 0.8, maxTokens: 2048 },
},
},
},
entries: {
audit: {
params: { temperature: 0.7, cacheRetention: "none" },
models: { [modelRef]: { params } },
},
},
},
},
provider,
modelId,
agentId: "audit",
});
expect(result).toEqual(
expect.objectContaining({
maxTokens: 2048,
cacheRetention: "none",
...params,
}),
);
},
);
it("ignores model params belonging to another agent", () => {
it("applies agent-model params at the narrowest config precedence", () => {
const result = resolveExtraParams({
cfg: {
agents: {
entries: {
audit: {
models: {
"anthropic/claude-sonnet-4-6": { params: { temperature: 0.2 } },
defaults: {
params: { temperature: 0.9, topP: 0.9, cacheRetention: "long" },
models: {
"anthropic/claude-sonnet-4-6": {
params: { temperature: 0.8, topP: 0.8, maxTokens: 2048 },
},
},
},
entries: {
audit: {
params: { temperature: 0.7, cacheRetention: "none" },
models: {
"anthropic/claude-sonnet-4-6": {
params: { temperature: 0.2, topP: 0.3, maxTokens: 321 },
},
},
},
main: {},
},
},
},
provider: "anthropic",
modelId: "claude-sonnet-4-6",
agentId: "main",
agentId: "audit",
});
expect(result).toBeUndefined();
expect(result).toEqual({
temperature: 0.2,
topP: 0.3,
maxTokens: 321,
cacheRetention: "none",
});
});
it("ignores the selected agent's params for another model", () => {
const result = resolveExtraParams({
cfg: {
agents: {
entries: {
audit: {
models: {
"anthropic/claude-sonnet-4-6": { params: { temperature: 0.2 } },
},
it("isolates agent-model params to the selected agent and model", () => {
const cfg = {
agents: {
entries: {
audit: {
models: {
"anthropic/claude-sonnet-4-6": { params: { temperature: 0.2 } },
},
},
main: {},
},
},
};
const otherAgent = resolveExtraParams({
cfg,
provider: "anthropic",
modelId: "claude-sonnet-4-6",
agentId: "main",
});
const otherModel = resolveExtraParams({
cfg,
provider: "anthropic",
modelId: "claude-opus-4-6",
agentId: "audit",
});
expect(result).toBeUndefined();
expect(otherAgent).toBeUndefined();
expect(otherModel).toBeUndefined();
});
it("preserves higher-precedence agent parallelToolCalls override across alias styles", () => {
@@ -205,14 +205,14 @@ const makeAgentOverrideOnlyFallbackConfig = (agentId: string): OpenClawConfig =>
fallbacks: [],
},
},
entries: {
main: { default: true },
[agentId]: {
list: [
{
id: agentId,
model: {
fallbacks: ["openai/mock-2"],
},
},
},
],
},
models: {
providers: {
@@ -118,18 +118,16 @@ export function resolveExtraParams(params: {
modelId: string;
agentId?: string;
}): Record<string, unknown> | undefined {
const { defaultParams, modelParams, agentEntryParams, agentModelParams } =
resolveModelExtraParamSources({
config: params.cfg,
provider: params.provider,
modelId: params.modelId,
agentId: params.agentId,
});
const scopedParams = [defaultParams, modelParams, agentEntryParams, agentModelParams];
const { paramSources } = resolveModelExtraParamSources({
config: params.cfg,
provider: params.provider,
modelId: params.modelId,
agentId: params.agentId,
});
const merged = Object.assign({}, ...scopedParams);
const merged = Object.assign({}, ...paramSources);
const resolvedParallelToolCalls = resolveAliasedParamValue(
scopedParams,
paramSources,
"parallel_tool_calls",
"parallelToolCalls",
);
@@ -139,7 +137,7 @@ export function resolveExtraParams(params: {
}
const resolvedTextVerbosity = resolveAliasedParamValue(
scopedParams.slice(1),
paramSources.slice(1),
"text_verbosity",
"textVerbosity",
);
@@ -149,7 +147,7 @@ export function resolveExtraParams(params: {
}
const resolvedResponseFormat = resolveAliasedParamValue(
scopedParams,
paramSources,
"response_format",
"responseFormat",
);
@@ -159,11 +157,11 @@ export function resolveExtraParams(params: {
}
canonicalizeMaxTokensParam({
merged,
sources: scopedParams,
sources: paramSources,
});
const resolvedCachedContent = resolveAliasedParamValue(
scopedParams,
paramSources,
"cached_content",
"cachedContent",
);
@@ -172,7 +170,7 @@ export function resolveExtraParams(params: {
delete merged.cached_content;
}
if (params.provider === "openrouter") {
canonicalizeOpenRouterResponseCacheParams(merged, scopedParams);
canonicalizeOpenRouterResponseCacheParams(merged, paramSources);
}
applyDefaultOpenAIGptRuntimeParams(params, merged);
+5 -4
View File
@@ -200,9 +200,10 @@ describe("resolveFastModeState", () => {
});
it.each([
["fastSeconds", { fastSeconds: 15 }],
["fast_seconds", { fast_seconds: 15 }],
])("uses model %s alias for auto cutoff", (_label, params) => {
["fastSeconds", { fastSeconds: 15 }, 15],
["fast_seconds", { fast_seconds: 15 }, 15],
["fast_auto_on_seconds before fastSeconds", { fast_auto_on_seconds: 20, fastSeconds: 15 }, 20],
])("uses model %s alias for auto cutoff", (_label, params, expected) => {
const cfg = {
agents: {
defaults: {
@@ -221,7 +222,7 @@ describe("resolveFastModeState", () => {
expect(state.mode).toBe("auto");
expect(state.source).toBe("config");
expect(state.fastAutoOnSeconds).toBe(15);
expect(state.fastAutoOnSeconds).toBe(expected);
});
it("uses model config when the runtime passes a provider-qualified model ref", () => {
+22 -59
View File
@@ -5,13 +5,13 @@ import type { FastMode } from "@openclaw/normalization-core/string-coerce";
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,
resolveFastModeModelParams,
} from "../shared/fast-mode.js";
import { DEFAULT_FAST_MODE_AUTO_ON_SECONDS, type FastModeSource } from "../shared/fast-mode.js";
import { resolveAgentConfig } from "./agent-scope.js";
import { resolveModelExtraParamSources } from "./model-extra-params.js";
import {
FAST_MODE_CUTOFF_MODEL_PARAM_KEYS,
FAST_MODE_MODEL_PARAM_KEYS,
resolveModelExtraParamValue,
} from "./model-extra-params.js";
export {
DEFAULT_FAST_MODE_AUTO_ON_SECONDS,
@@ -34,53 +34,6 @@ type FastModeState = {
fastAutoOnSeconds: number;
};
function resolveConfiguredFastModeParamSources(params: {
cfg: OpenClawConfig | undefined;
provider: string;
model: string;
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. */
export function resolveFastModeState(params: {
cfg: OpenClawConfig | undefined;
@@ -89,8 +42,21 @@ export function resolveFastModeState(params: {
agentId?: string;
sessionEntry?: Pick<SessionEntry, "fastMode"> | undefined;
}): FastModeState {
const configuredParamSources = resolveConfiguredFastModeParamSources(params);
const fastAutoOnSeconds = resolveConfiguredFastModeAutoOnSeconds(configuredParamSources);
const modelParamContext = {
config: params.cfg,
provider: params.provider,
modelId: params.model,
agentId: params.agentId,
};
const configuredAutoOnSeconds = resolveModelExtraParamValue(
modelParamContext,
FAST_MODE_CUTOFF_MODEL_PARAM_KEYS,
(value) => typeof value === "number" && Number.isInteger(value) && value > 0,
);
const fastAutoOnSeconds =
typeof configuredAutoOnSeconds === "number"
? configuredAutoOnSeconds
: DEFAULT_FAST_MODE_AUTO_ON_SECONDS;
const sessionOverride = normalizeFastMode(params.sessionEntry?.fastMode);
if (sessionOverride !== undefined) {
return {
@@ -115,10 +81,7 @@ export function resolveFastModeState(params: {
};
}
const configuredRaw = resolveConfiguredFastModeValue(configuredParamSources, [
"fastMode",
"fast_mode",
]);
const configuredRaw = resolveModelExtraParamValue(modelParamContext, FAST_MODE_MODEL_PARAM_KEYS);
const configured = normalizeFastMode(configuredRaw as string | boolean | null | undefined);
if (configured !== undefined) {
return {
+69 -28
View File
@@ -4,20 +4,22 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
import { modelKey } from "../shared/model-key.js";
import { resolveAgentConfig } from "./agent-scope-config.js";
type ModelExtraParamSources = {
export type ModelExtraParamSources = {
defaultParams?: Record<string, unknown>;
modelParams?: Record<string, unknown>;
agentParams?: Record<string, unknown>;
agentEntryParams?: Record<string, unknown>;
agentModelParams?: Record<string, unknown>;
paramSources: Array<Record<string, unknown> | undefined>;
};
const FAST_MODE_CUTOFF_MODEL_PARAM_KEYS = new Set([
export const FAST_MODE_MODEL_PARAM_KEYS = ["fastMode", "fast_mode"] as const;
export const FAST_MODE_CUTOFF_MODEL_PARAM_KEYS = [
"fastAutoOnSeconds",
"fastSeconds",
"fast_auto_on_seconds",
"fastSeconds",
"fast_seconds",
]);
] as const;
const FAST_MODE_CUTOFF_MODEL_PARAM_KEY_SET = new Set<string>(FAST_MODE_CUTOFF_MODEL_PARAM_KEYS);
// Native harnesses receive recognized values as typed run controls. Other value
// shapes with the same keys remain authored provider request parameters.
@@ -30,11 +32,11 @@ export function isAgentRuntimeModelParam(key: string, value: unknown): boolean {
(typeof value === "string" && normalizeThinkLevel(value) !== undefined)
);
}
if (key === "fastMode" || key === "fast_mode") {
if (FAST_MODE_MODEL_PARAM_KEYS.some((candidate) => candidate === key)) {
return normalizeFastMode(value) !== undefined;
}
return (
FAST_MODE_CUTOFF_MODEL_PARAM_KEYS.has(key) &&
FAST_MODE_CUTOFF_MODEL_PARAM_KEY_SET.has(key) &&
typeof value === "number" &&
Number.isInteger(value) &&
value > 0
@@ -69,31 +71,70 @@ export function resolveModelExtraParamSources(params: {
? (agentConfig?.models?.[canonicalKey]?.params ??
(legacyKey ? agentConfig?.models?.[legacyKey]?.params : undefined))
: undefined;
// Keep the merged exact-key projection for classifiers and diagnostics.
// Request construction uses the separate records so alias precedence survives.
const agentParams = agentModelParams
? { ...agentEntryParams, ...agentModelParams }
: agentEntryParams;
return { defaultParams, modelParams, agentParams, agentEntryParams, agentModelParams };
const paramSources = [defaultParams, modelParams, agentEntryParams, agentModelParams];
return { defaultParams, modelParams, agentEntryParams, agentModelParams, paramSources };
}
function resolveModelExtraParamEntryFromSources(
sources: readonly (Record<string, unknown> | undefined)[],
keys: readonly string[],
accepts?: (value: unknown) => boolean,
): { key: string; value: unknown; sourceIndex: number } | undefined {
for (let sourceIndex = sources.length - 1; sourceIndex >= 0; sourceIndex -= 1) {
const source = sources[sourceIndex];
for (const key of keys) {
if (!source || !Object.hasOwn(source, key)) {
continue;
}
const value = source[key];
if (!accepts || accepts(value)) {
return { key, value, sourceIndex };
}
}
}
return undefined;
}
/** Resolves the effective parameter set with the winning config scope retained. */
export function resolveEffectiveModelExtraParams(sources: ModelExtraParamSources): Array<{
key: string;
effectiveKey: string;
value: unknown;
sourceIndex: number;
}> {
const effective = new Map<
string,
{ key: string; effectiveKey: string; value: unknown; sourceIndex: number }
>();
sources.paramSources.forEach((source, sourceIndex) => {
for (const [key, value] of Object.entries(source ?? {})) {
effective.set(key, { key, effectiveKey: key, value, sourceIndex });
}
});
for (const aliases of [FAST_MODE_MODEL_PARAM_KEYS, FAST_MODE_CUTOFF_MODEL_PARAM_KEYS]) {
const entry = resolveModelExtraParamEntryFromSources(sources.paramSources, aliases);
for (const alias of aliases) {
effective.delete(alias);
}
if (entry) {
effective.set(aliases[0], { ...entry, effectiveKey: aliases[0] });
}
}
return [...effective.values()];
}
/** Resolves one authored parameter across the canonical config precedence. */
export function resolveModelExtraParamValue(
params: Parameters<typeof resolveModelExtraParamSources>[0],
key: string,
key: string | readonly string[],
accepts?: (value: unknown) => boolean,
): 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;
return resolveModelExtraParamEntryFromSources(
sources.paramSources,
typeof key === "string" ? [key] : key,
accepts,
)?.value;
}
/** Returns whether embedded OpenClaw would apply authored provider request parameters. */
@@ -101,7 +142,7 @@ export function hasAuthoredProviderRequestParams(
params: Parameters<typeof resolveModelExtraParamSources>[0],
): boolean {
const sources = resolveModelExtraParamSources(params);
return Object.entries(
Object.assign({}, sources.defaultParams, sources.modelParams, sources.agentParams),
).some(([key, value]) => !isAgentRuntimeModelParam(key, value));
return resolveEffectiveModelExtraParams(sources).some(
({ effectiveKey, value }) => !isAgentRuntimeModelParam(effectiveKey, value),
);
}
+67 -71
View File
@@ -11,38 +11,6 @@ import {
} from "./openai-routing.js";
const CODEX_RUNTIME_CONTROL_SCOPES = ["model", "global", "agent", "agent-model"] as const;
const CODEX_RUNTIME_CONTROL_CASES: ReadonlyArray<readonly [string, Record<string, unknown>]> = [
["thinking off", { thinking: "off" }],
["thinking minimal", { thinking: "minimal" }],
["thinking low", { thinking: "low" }],
["thinking medium", { thinking: "medium" }],
["thinking high", { thinking: "high" }],
["thinking xhigh", { thinking: "xhigh" }],
["thinking adaptive", { thinking: "adaptive" }],
["thinking max", { thinking: "max" }],
["thinking ultra", { thinking: "ultra" }],
["thinking false", { thinking: false }],
["thinking disabled", { thinking: "disabled" }],
["thinking none", { thinking: "none" }],
["fastMode on", { fastMode: true }],
["fastMode off", { fastMode: false }],
["fastMode auto", { fastMode: "auto" }],
["fast_mode", { fast_mode: true }],
["fastAutoOnSeconds", { fastMode: "auto", fastAutoOnSeconds: 30 }],
["fast_auto_on_seconds", { fastMode: "auto", fast_auto_on_seconds: 30 }],
["fastSeconds", { fastMode: "auto", fastSeconds: 30 }],
["fast_seconds", { fastMode: "auto", fast_seconds: 30 }],
];
const OPENAI_PROVIDER_REQUEST_PARAM_CASES: ReadonlyArray<
readonly [string, Record<string, unknown>]
> = [
["provider-native thinking", { thinking: { type: "enabled", budget_tokens: 2_048 } }],
["invalid fast mode", { fastMode: { enabled: true } }],
["invalid fast cutoff", { fastAutoOnSeconds: "30" }],
["provider temperature", { temperature: 0.4 }],
["provider service tier", { serviceTier: "priority" }],
["provider transport", { transport: "sse" }],
];
function createScopedOpenAIRoutingConfig(
scope: (typeof CODEX_RUNTIME_CONTROL_SCOPES)[number],
@@ -90,75 +58,103 @@ describe("OpenAI runtime routing policy", () => {
).toBe(true);
});
it.each(
CODEX_RUNTIME_CONTROL_CASES.flatMap(([label, params]) =>
CODEX_RUNTIME_CONTROL_SCOPES.map((scope) => ({
label,
params,
scope,
})),
),
)("keeps Codex for $label controls at $scope scope", ({ params, scope }) => {
const { config, agentId } = createScopedOpenAIRoutingConfig(scope, params);
it.each([
["thinking", { thinking: "xhigh" }],
["fastMode", { fastMode: true }],
["fast_mode", { fast_mode: true }],
["fastAutoOnSeconds", { fastMode: "auto", fastAutoOnSeconds: 30 }],
["fast_auto_on_seconds", { fastMode: "auto", fast_auto_on_seconds: 30 }],
["fastSeconds", { fastMode: "auto", fastSeconds: 30 }],
["fast_seconds", { fastMode: "auto", fast_seconds: 30 }],
])("keeps Codex for model-scoped %s controls", (_label, params) => {
const { config } = createScopedOpenAIRoutingConfig("model", params);
expect(
resolveOpenAIImplicitAgentRuntime({
provider: "openai",
modelId: "gpt-5.6-sol",
config,
agentId,
env: {},
}),
).toBe("codex");
expect(
modelSelectionShouldEnsureCodexPlugin({
model: "openai/gpt-5.6-sol",
config,
agentId,
}),
).toBe(true);
});
it.each(
OPENAI_PROVIDER_REQUEST_PARAM_CASES.flatMap(([label, params]) =>
CODEX_RUNTIME_CONTROL_SCOPES.map((scope) => ({
label,
params,
scope,
})),
),
)("keeps $label values at $scope scope on the OpenClaw runtime", ({ params, scope }) => {
const { config, agentId } = createScopedOpenAIRoutingConfig(scope, params);
it.each(["global", "agent", "agent-model"] as const)(
"keeps typed native controls at %s scope on Codex",
(scope) => {
const { config, agentId } = createScopedOpenAIRoutingConfig(scope, { thinking: "high" });
expect(
resolveOpenAIImplicitAgentRuntime({
provider: "openai",
modelId: "gpt-5.6-sol",
config,
agentId,
env: {},
}),
).toBe("codex");
expect(
modelSelectionShouldEnsureCodexPlugin({
model: "openai/gpt-5.6-sol",
config,
agentId,
}),
).toBe(true);
},
);
it.each([
["provider-native thinking", { thinking: { type: "enabled", budget_tokens: 2_048 } }],
["invalid fast mode", { fastMode: { enabled: true } }],
["invalid fast cutoff", { fastAutoOnSeconds: "30" }],
])("keeps %s values on the OpenClaw runtime", (_label, params) => {
const { config } = createScopedOpenAIRoutingConfig("model", params);
expect(
resolveOpenAIImplicitAgentRuntime({
provider: "openai",
modelId: "gpt-5.6-sol",
config,
agentId,
env: {},
}),
).toBe("openclaw");
expect(
modelSelectionShouldEnsureCodexPlugin({
model: "openai/gpt-5.6-sol",
config,
agentId,
}),
).toBe(false);
});
it("classifies only the effective value after agent-model parameter precedence", () => {
it.each(["global", "agent", "agent-model"] as const)(
"keeps authored provider params at %s scope on OpenClaw",
(scope) => {
const { config, agentId } = createScopedOpenAIRoutingConfig(scope, { temperature: 0.4 });
expect(
resolveOpenAIImplicitAgentRuntime({
provider: "openai",
modelId: "gpt-5.6-sol",
config,
agentId,
env: {},
}),
).toBe("openclaw");
expect(
modelSelectionShouldEnsureCodexPlugin({
model: "openai/gpt-5.6-sol",
config,
agentId,
}),
).toBe(false);
},
);
it("classifies the effective alias after agent-model parameter precedence", () => {
const modelKey = "openai/gpt-5.6-sol";
const config = {
agents: {
defaults: {
params: { thinking: { type: "enabled", budget_tokens: 2_048 } },
params: { fastMode: { enabled: true } },
},
entries: {
audit: {
models: {
[modelKey]: { params: { thinking: "high" } },
[modelKey]: { params: { fast_mode: true } },
},
},
},
+3 -5
View File
@@ -1,7 +1,7 @@
/** Shared command registry builders used by browser-safe and runtime command lists. */
import { normalizeOptionalLowercaseString } from "../../packages/normalization-core/src/string-coerce.js";
import { normalizeStringEntries } from "../../packages/normalization-core/src/string-normalization.js";
import { formatFastModeAutoLabel, resolveFastModeModelAutoOnSeconds } from "../shared/fast-mode.js";
import { formatFastModeAutoLabel } from "../shared/fast-mode.js";
import { COMMAND_ARG_FORMATTERS } from "./commands-args.js";
import type {
ChatCommandDefinition,
@@ -590,14 +590,12 @@ export function buildBuiltinChatCommands(
defineBuiltinCommand("fast", "Toggle fast mode.", "options", "standard", {
args: [
defineCommandArgument("mode", "on, off, auto, default, or status", {
choices: ({ cfg, provider, model }) => [
choices: ({ fastAutoOnSeconds }) => [
"on",
"off",
{
value: "auto",
label: formatFastModeAutoLabel({
fastAutoOnSeconds: resolveFastModeModelAutoOnSeconds({ cfg, provider, model }),
}),
label: formatFastModeAutoLabel({ fastAutoOnSeconds }),
},
"default",
"status",
+13 -4
View File
@@ -548,22 +548,31 @@ describe("commands registry", () => {
cfg: {
agents: {
defaults: {
model: "openai-codex/gpt-5.5",
model: "openai/gpt-5.5",
models: {
"openai-codex/gpt-5.5": {
"openai/gpt-5.5": {
params: { fastMode: "auto", fastAutoOnSeconds: 30 },
},
},
},
entries: {
audit: {
params: { fastAutoOnSeconds: 20 },
models: {
"openai/gpt-5.5": { params: { fast_seconds: 15 } },
},
},
},
},
} as never,
provider: "openai-codex",
provider: "openai",
model: "gpt-5.5",
agentId: "audit",
});
expect(menu.choices).toEqual([
{ label: "on", value: "on" },
{ label: "off", value: "off" },
{ label: "auto (30 sec)", value: "auto" },
{ label: "auto (15 sec)", value: "auto" },
{ label: "default", value: "default" },
{ label: "status", value: "status" },
]);
+16 -3
View File
@@ -2,6 +2,7 @@
import { expectDefined } from "@openclaw/normalization-core";
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
import { resolveFastModeState } from "../agents/fast-mode.js";
import {
buildConfiguredModelCatalog,
resolveConfiguredModelRef,
@@ -331,6 +332,7 @@ export function resolveCommandArgChoices(params: {
cfg?: OpenClawConfig;
provider?: string;
model?: string;
agentId?: string;
agentRuntime?: string;
catalog?: ThinkingCatalogEntry[];
}): ResolvedCommandArgChoice[] {
@@ -343,11 +345,19 @@ export function resolveCommandArgChoices(params: {
? provided
: (() => {
const defaults = resolveDefaultCommandContext(cfg);
const provider = params.provider ?? defaults.provider;
const model = params.model ?? defaults.model;
const context: CommandArgChoiceContext = {
cfg,
provider: params.provider ?? defaults.provider,
model: params.model ?? defaults.model,
provider,
model,
agentId: params.agentId,
agentRuntime: params.agentRuntime,
fastAutoOnSeconds:
command.key === "fast"
? resolveFastModeState({ cfg, provider, model, agentId: params.agentId })
.fastAutoOnSeconds
: undefined,
catalog: params.catalog ?? (cfg ? buildConfiguredModelCatalog({ cfg }) : undefined),
command,
arg,
@@ -366,10 +376,11 @@ export function resolveCommandArgMenu(params: {
cfg?: OpenClawConfig;
provider?: string;
model?: string;
agentId?: string;
agentRuntime?: string;
catalog?: ThinkingCatalogEntry[];
}): { arg: CommandArgDefinition; choices: ResolvedCommandArgChoice[]; title?: string } | null {
const { command, args, cfg, provider, model, agentRuntime, catalog } = params;
const { command, args, cfg, provider, model, agentId, agentRuntime, catalog } = params;
if (!command.args || !command.argsMenu) {
return null;
}
@@ -388,6 +399,7 @@ export function resolveCommandArgMenu(params: {
cfg,
provider,
model,
agentId,
agentRuntime,
catalog: resolvedCatalog,
}).length > 0,
@@ -412,6 +424,7 @@ export function resolveCommandArgMenu(params: {
cfg,
provider,
model,
agentId,
agentRuntime,
catalog: resolvedCatalog,
});
@@ -33,7 +33,9 @@ export type CommandArgChoiceContext = {
cfg?: OpenClawConfig;
provider?: string;
model?: string;
agentId?: string;
agentRuntime?: string;
fastAutoOnSeconds?: number;
catalog?: ThinkingCatalogEntry[];
command: ChatCommandDefinition;
arg: CommandArgDefinition;
@@ -460,10 +460,10 @@ describe("collectCodexRouteWarnings", () => {
},
entries: {
coder: {
params: { topP: 0.8, fastMode: "auto", temperature: 0.6 },
params: { topP: 0.8, fastMode: { enabled: true }, temperature: 0.6 },
models: {
"openai/gpt-5.6-sol": {
params: { temperature: 0.1, thinking: "medium", topK: 40 },
params: { fast_mode: true, temperature: 0.1, thinking: "medium", topK: 40 },
},
},
},
@@ -483,10 +483,10 @@ describe("collectCodexRouteWarnings", () => {
expect(result.warnings.join("\n")).toContain(
"agents.defaults.models.openai/gpt-5.6-sol.params.serviceTier",
);
expect(result.warnings.join("\n")).toContain(
expect(result.warnings.join("\n")).not.toContain(
"agents.defaults.models.openai/gpt-5.6-sol.params.temperature",
);
expect(result.warnings.join("\n")).toContain("agents.defaults.params.temperature");
expect(result.warnings.join("\n")).not.toContain("agents.defaults.params.temperature");
expect(result.warnings.join("\n")).toContain("agents.entries.coder.params.topP");
expect(result.warnings.join("\n")).toContain(
"agents.entries.coder.models.openai/gpt-5.6-sol.params.temperature",
@@ -6,6 +6,7 @@ import {
} from "@openclaw/normalization-core/string-coerce";
import {
isAgentRuntimeModelParam,
resolveEffectiveModelExtraParams,
resolveModelExtraParamSources,
} from "../../../agents/model-extra-params.js";
import { resolveModelRuntimePolicy } from "../../../agents/model-runtime-policy.js";
@@ -197,11 +198,8 @@ function collectCodexModelParamHits(
): CodexModelParamHit[] {
const hits: CodexModelParamHit[] = [];
const seen = new Set<string>();
const agentEntries = new Map(
listMutableCodexRouteAgentEntries(cfg).map(({ agent, agentId, path }) => [
agentId,
{ agent, path },
]),
const agentPaths = new Map(
listMutableCodexRouteAgentEntries(cfg).map(({ agentId, path }) => [agentId, path]),
);
for (const route of collectCodexRuntimeRouteHits(cfg, env)) {
const parsed = parseCodexRouteModelRef(route.canonicalModel);
@@ -214,11 +212,6 @@ function collectCodexModelParamHits(
modelId: parsed.modelId,
agentId: route.agentId,
});
const agentEntry = route.agentId ? agentEntries.get(route.agentId) : undefined;
const agentModels = asMutableRecord(agentEntry?.agent.models);
const agentModelParams = asMutableRecord(
asMutableRecord(agentModels?.[route.canonicalModel])?.params,
);
const modelParams = sources.modelParams;
const fastModes = ownValues(modelParams ?? {}, FAST_MODE_PARAM_KEYS);
const serviceTiers = ownValues(modelParams ?? {}, SERVICE_TIER_PARAM_KEYS);
@@ -228,55 +221,36 @@ function collectCodexModelParamHits(
serviceTiers.length > 0 &&
serviceTiers.every((configured) => normalizeString(configured) === "priority") &&
modelUsesCodexForEveryAgent(cfg, route.canonicalModel);
const paramSources = [
{
params: sources.defaultParams,
path: "agents.defaults.params",
modelScoped: false,
agentModelParams: undefined,
},
{
params: modelParams,
path: `agents.defaults.models.${route.canonicalModel}.params`,
modelScoped: true,
agentModelParams: undefined,
},
...(route.agentId
? [
{
params: sources.agentParams,
path: agentEntry?.path ?? `agents.entries.${route.agentId}`,
modelScoped: false,
agentModelParams,
},
]
: []),
const agentPath = route.agentId
? (agentPaths.get(route.agentId) ?? `agents.entries.${route.agentId}`)
: undefined;
const sourcePaths = [
"agents.defaults.params",
`agents.defaults.models.${route.canonicalModel}.params`,
agentPath ? `${agentPath}.params` : undefined,
agentPath ? `${agentPath}.models.${route.canonicalModel}.params` : undefined,
];
for (const source of paramSources) {
for (const [key, paramValue] of Object.entries(source.params ?? {})) {
if (isAgentRuntimeModelParam(key, paramValue)) {
continue;
}
const sourcePath = Object.hasOwn(source.agentModelParams ?? {}, key)
? `${source.path}.models.${route.canonicalModel}.params`
: source.modelScoped || source.path === "agents.defaults.params"
? source.path
: `${source.path}.params`;
const path = `${sourcePath}.${key}`;
if (seen.has(path)) {
continue;
}
seen.add(path);
hits.push({
key,
path,
modelRef: route.canonicalModel,
removable:
source.modelScoped &&
canRemoveServiceTier &&
SERVICE_TIER_PARAM_KEYS.some((alias) => alias === key),
});
for (const { key, effectiveKey, value, sourceIndex } of resolveEffectiveModelExtraParams(
sources,
)) {
const sourcePath = sourcePaths[sourceIndex];
if (!sourcePath || isAgentRuntimeModelParam(effectiveKey, value)) {
continue;
}
const path = `${sourcePath}.${key}`;
if (seen.has(path)) {
continue;
}
seen.add(path);
hits.push({
key,
path,
modelRef: route.canonicalModel,
removable:
sourceIndex === 1 &&
canRemoveServiceTier &&
SERVICE_TIER_PARAM_KEYS.some((alias) => alias === key),
});
}
}
return hits;