fix: honor wildcard model runtime policy

This commit is contained in:
Peter Steinberger
2026-05-15 19:58:49 +01:00
parent dd4613a268
commit 638bfc0bf5
4 changed files with 162 additions and 11 deletions

View File

@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
- Agents/OpenAI-compatible: honor per-model `max_completion_tokens`/`max_tokens` params in embedded OpenAI-completions runs so high-token Kimi-style routes keep their configured completion cap. Fixes #82230. Thanks @albert-zen.
- Agents/local: install a local gateway request scope around trusted `openclaw agent --local` runs, so subagent completion announces can use in-process gateway dispatch without crashing. Fixes #82140. Thanks @Kushmaro.
- Discord: validate message-read results before normalizing channel history and report unexpected payloads with a Discord boundary error instead of `map is not a function`. Fixes #82252. Thanks @jessewunderlich.
- Agents/runtime: apply `agents.defaults.models["provider/*"].agentRuntime` as provider-wide model runtime policy while preserving exact model runtime precedence. Fixes #82243. Thanks @rendrag-git.
- Telegram/active-memory: run blocking memory recall through the Telegram provider for direct-message turns even when the hook context carries the raw chat id, preventing embedded recall from launching against an invalid numeric channel. Fixes #82177. Thanks @cslash-zz.
- Control UI/WebChat: keep optimistic image messages from embedding large inline `data:` previews and preserve image-only user turns in chat history, avoiding browser stack overflows when sending image attachments. Fixes #82182. Thanks @ExploreSheep.
- Agents/media: preserve message-tool-only delivery for generated music and video completion handoffs, so group/channel completions do not finish without posting the generated attachment.

View File

@@ -220,6 +220,18 @@ describe("runAgentHarnessAttempt", () => {
expect(piRunAttempt).toHaveBeenCalledTimes(1);
});
it("honors provider wildcard PI runtime policy for OpenAI agent model runs", async () => {
registerSuccessfulCodexHarness();
const result = await runAgentHarnessAttempt({
...createAttemptParams(agentModelRuntimeConfig("openai/*", "pi")),
provider: "openai",
modelId: "gpt-5.4",
});
expect(result.sessionIdUsed).toBe("pi");
expect(piRunAttempt).toHaveBeenCalledTimes(1);
});
it("annotates non-ok harness result classifications for outer model fallback", async () => {
const classify = vi.fn<NonNullable<AgentHarness["classify"]>>(() => "empty" as const);
registerAgentHarness(

View File

@@ -0,0 +1,113 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveModelRuntimePolicy } from "./model-runtime-policy.js";
describe("resolveModelRuntimePolicy", () => {
it("honors provider wildcard agent model runtime policy entries", () => {
const config = {
agents: {
defaults: {
models: {
"vllm/*": { agentRuntime: { id: "pi" } },
},
},
},
} as OpenClawConfig;
expect(
resolveModelRuntimePolicy({
config,
provider: "vllm",
modelId: "qwen-local",
}),
).toEqual({
policy: { id: "pi" },
source: "model",
});
});
it("prefers exact agent model runtime policy entries over provider wildcards", () => {
const config = {
agents: {
defaults: {
models: {
"vllm/*": { agentRuntime: { id: "pi" } },
"vllm/qwen-local": { agentRuntime: { id: "codex" } },
},
},
},
} as OpenClawConfig;
expect(
resolveModelRuntimePolicy({
config,
provider: "vllm",
modelId: "qwen-local",
}),
).toEqual({
policy: { id: "codex" },
source: "model",
});
});
it("prefers exact provider model runtime policy over agent provider wildcards", () => {
const config = {
agents: {
defaults: {
models: {
"vllm/*": { agentRuntime: { id: "pi" } },
},
},
},
models: {
providers: {
vllm: {
models: [{ id: "qwen-local", agentRuntime: { id: "codex" } }],
},
},
},
} as OpenClawConfig;
expect(
resolveModelRuntimePolicy({
config,
provider: "vllm",
modelId: "qwen-local",
}),
).toEqual({
policy: { id: "codex" },
source: "model",
});
});
it("prefers agent provider wildcard runtime policy over provider runtime policy", () => {
const config = {
agents: {
defaults: {
models: {
"vllm/*": { agentRuntime: { id: "pi" } },
},
},
},
models: {
providers: {
vllm: {
agentRuntime: { id: "codex" },
models: [],
},
},
},
} as OpenClawConfig;
expect(
resolveModelRuntimePolicy({
config,
provider: "vllm",
modelId: "qwen-local",
}),
).toEqual({
policy: { id: "pi" },
source: "model",
});
});
});

View File

@@ -13,6 +13,8 @@ export type ResolvedModelRuntimePolicy = {
source?: ModelRuntimePolicySource;
};
type ModelEntryMatchKind = "none" | "exact" | "provider-wildcard";
function hasRuntimePolicy(value: AgentRuntimePolicyConfig | undefined): boolean {
return Boolean(value?.id?.trim());
}
@@ -63,26 +65,41 @@ function modelEntryMatches(params: {
provider: string | undefined;
modelId: string;
}): boolean {
return modelEntryMatchKind(params) === "exact";
}
function modelEntryMatchKind(params: {
entry: Pick<ModelDefinitionConfig, "id">;
provider: string | undefined;
modelId: string;
}): ModelEntryMatchKind {
const entryId = params.entry.id.trim();
if (entryId === params.modelId) {
return true;
return "exact";
}
const slash = entryId.indexOf("/");
if (slash <= 0) {
return false;
return "none";
}
return (
normalizeProviderId(entryId.slice(0, slash)) === normalizeProviderId(params.provider ?? "") &&
entryId.slice(slash + 1).trim() === params.modelId
);
if (normalizeProviderId(entryId.slice(0, slash)) !== normalizeProviderId(params.provider ?? "")) {
return "none";
}
const entryModelId = entryId.slice(slash + 1).trim();
if (entryModelId === params.modelId) {
return "exact";
}
if (entryModelId === "*") {
return "provider-wildcard";
}
return "none";
}
function modelKeyMatches(params: {
function modelKeyMatchKind(params: {
key: string;
provider: string | undefined;
modelId: string;
}): boolean {
return modelEntryMatches({
}): ModelEntryMatchKind {
return modelEntryMatchKind({
entry: { id: params.key },
provider: params.provider,
modelId: params.modelId,
@@ -95,6 +112,7 @@ function resolveAgentModelEntryRuntimePolicy(params: {
modelId?: string;
agentId?: string;
sessionKey?: string;
matchKind: Exclude<ModelEntryMatchKind, "none">;
}): ResolvedModelRuntimePolicy {
const modelId = normalizeModelIdForProvider(params.provider, params.modelId);
if (!params.config || !modelId) {
@@ -115,7 +133,7 @@ function resolveAgentModelEntryRuntimePolicy(params: {
for (const models of modelMaps) {
for (const [key, entry] of Object.entries(models ?? {})) {
if (
modelKeyMatches({ key, provider: params.provider, modelId }) &&
modelKeyMatchKind({ key, provider: params.provider, modelId }) === params.matchKind &&
hasRuntimePolicy(entry?.agentRuntime)
) {
return { policy: entry.agentRuntime, source: "model" };
@@ -146,7 +164,7 @@ export function resolveModelRuntimePolicy(params: {
agentId?: string;
sessionKey?: string;
}): ResolvedModelRuntimePolicy {
const agentModelPolicy = resolveAgentModelEntryRuntimePolicy(params);
const agentModelPolicy = resolveAgentModelEntryRuntimePolicy({ ...params, matchKind: "exact" });
if (agentModelPolicy.policy) {
return agentModelPolicy;
}
@@ -159,6 +177,13 @@ export function resolveModelRuntimePolicy(params: {
if (hasRuntimePolicy(modelConfig?.agentRuntime)) {
return { policy: modelConfig?.agentRuntime, source: "model" };
}
const agentWildcardModelPolicy = resolveAgentModelEntryRuntimePolicy({
...params,
matchKind: "provider-wildcard",
});
if (agentWildcardModelPolicy.policy) {
return agentWildcardModelPolicy;
}
if (hasRuntimePolicy(providerConfig?.agentRuntime)) {
return { policy: providerConfig?.agentRuntime, source: "provider" };
}