Files
openclaw/src/agents/utility-model.test.ts
Peter Steinberger 97bbbc2271 feat(agents): derive a provider-declared default utility model when unset (#103769)
* feat(agents): derive a provider-declared default utility model when unset

When agents.defaults.utilityModel is not set, utility tasks (titles, progress
narration) now use the primary provider's declared small model
(modelCatalog.providers.<id>.defaultUtilityModel: OpenAI -> gpt-5.6-luna,
Anthropic -> claude-haiku-4-5). Auth is inherent because derivation follows
the agent's primary provider. Setting utilityModel to an empty string
disables utility routing entirely; narration turns on automatically when a
default resolves and stays off otherwise.

Formatting/lint/tests verified on Testbox (oxfmt, oxlint, plugins:inventory,
docs:map, import-cycles, check:test-types, 4 test files).

* fix(ai): drop the temperature parameter for models that reject it

The GPT-5.6 family 400s on temperature via the Responses API (live-verified:
gpt-5.6-luna/-terra reject it; gpt-5.5 and gpt-5.4-mini/nano accept it).
supportsOpenAITemperature gates all three OpenAI payload builders, with a
catalog compat override (compat.supportsTemperature) declared for the 5.6
family in the openai manifest. Without this, utility tasks (titles,
narration) would fail once gpt-5.6-luna becomes the derived default.

Gates on Testbox: oxfmt, oxlint, plugins:inventory, import-cycles,
check:test-types, 6 test files. Live proof on Testbox: gpt-5.6-luna and
claude-haiku-4-5 one-shot completions succeed with temperature requested.

* fix(agents): carry the primary model's auth profile onto the derived utility default

A primary like openai/gpt-5.5@work previously reached utility tasks via the
profiled fallback; the derived default shares the provider, so its trailing
auth profile carries over instead of silently switching to default
credentials (Codex review). Gates on Testbox: oxfmt, check:test-types, tests.

* docs: realign the manifest provider-fields table after adding defaultUtilityModel
2026-07-10 17:40:11 +01:00

152 lines
4.8 KiB
TypeScript

// Utility-model resolution tests cover explicit/disabled/auto settings and
// provider-declared default derivation.
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";
import {
readUtilityModelSetting,
resolveProviderDefaultUtilityModelRef,
resolveUtilityModelRefForAgent,
} from "./utility-model.js";
function snapshotWithDefaults(defaults: Record<string, string>): PluginMetadataSnapshot {
const plugins = Object.entries(defaults).map(([provider, defaultUtilityModel], index) => ({
id: `plugin-${index}`,
modelCatalog: {
providers: {
[provider]: { defaultUtilityModel, models: [{ id: defaultUtilityModel }] },
},
},
}));
return { plugins } as unknown as PluginMetadataSnapshot;
}
describe("readUtilityModelSetting", () => {
it("distinguishes unset, explicit, and empty-string disable", () => {
expect(readUtilityModelSetting({} as OpenClawConfig, "main")).toEqual({ kind: "auto" });
expect(
readUtilityModelSetting(
{ agents: { defaults: { utilityModel: " openai/gpt-5.4-mini " } } } as OpenClawConfig,
"main",
),
).toEqual({ kind: "explicit", modelRef: "openai/gpt-5.4-mini" });
expect(
readUtilityModelSetting(
{ agents: { defaults: { utilityModel: "" } } } as OpenClawConfig,
"main",
),
).toEqual({ kind: "disabled" });
expect(
readUtilityModelSetting(
{ agents: { defaults: { utilityModel: " " } } } as OpenClawConfig,
"main",
),
).toEqual({ kind: "disabled" });
});
it("lets an agent-level empty string disable a defaults-level model", () => {
const cfg = {
agents: {
defaults: { utilityModel: "openai/gpt-5.4-mini" },
list: [{ id: "ops", utilityModel: "" }],
},
} as OpenClawConfig;
expect(readUtilityModelSetting(cfg, "ops")).toEqual({ kind: "disabled" });
expect(readUtilityModelSetting(cfg, "main")).toEqual({
kind: "explicit",
modelRef: "openai/gpt-5.4-mini",
});
});
});
describe("resolveProviderDefaultUtilityModelRef", () => {
it("reads the provider-declared default from the metadata snapshot", () => {
const metadataSnapshot = snapshotWithDefaults({
openai: "gpt-5.6-luna",
anthropic: "claude-haiku-4-5",
});
expect(
resolveProviderDefaultUtilityModelRef({
cfg: {} as OpenClawConfig,
provider: "Anthropic",
metadataSnapshot,
}),
).toBe("anthropic/claude-haiku-4-5");
expect(
resolveProviderDefaultUtilityModelRef({
cfg: {} as OpenClawConfig,
provider: "ollama",
metadataSnapshot,
}),
).toBeUndefined();
});
});
describe("resolveUtilityModelRefForAgent", () => {
const metadataSnapshot = snapshotWithDefaults({
openai: "gpt-5.6-luna",
anthropic: "claude-haiku-4-5",
});
it("passes explicit config through untouched", () => {
const cfg = {
agents: { defaults: { utilityModel: "openrouter/mistralai/mistral-small" } },
} as OpenClawConfig;
expect(resolveUtilityModelRefForAgent({ cfg, agentId: "main", metadataSnapshot })).toBe(
"openrouter/mistralai/mistral-small",
);
});
it("returns undefined when utility routing is disabled", () => {
const cfg = { agents: { defaults: { utilityModel: "" } } } as OpenClawConfig;
expect(
resolveUtilityModelRefForAgent({ cfg, agentId: "main", metadataSnapshot }),
).toBeUndefined();
});
it("derives the provider default from the agent's primary model", () => {
const cfg = {
agents: { defaults: { model: "anthropic/claude-fable-5" } },
} as OpenClawConfig;
expect(resolveUtilityModelRefForAgent({ cfg, agentId: "main", metadataSnapshot })).toBe(
"anthropic/claude-haiku-4-5",
);
});
it("carries the primary model's auth profile onto the derived default", () => {
const cfg = {
agents: { defaults: { model: "openai/gpt-5.5@work" } },
} as OpenClawConfig;
expect(resolveUtilityModelRefForAgent({ cfg, agentId: "main", metadataSnapshot })).toBe(
"openai/gpt-5.6-luna@work",
);
});
it("prefers a caller-resolved primary provider over re-derivation", () => {
expect(
resolveUtilityModelRefForAgent({
cfg: {} as OpenClawConfig,
agentId: "main",
primaryProvider: "openai",
metadataSnapshot,
}),
).toBe("openai/gpt-5.6-luna");
});
it("returns undefined for providers without a declared default", () => {
const cfg = {
agents: { defaults: { model: "ollama/llama-4-70b" } },
} as OpenClawConfig;
expect(
resolveUtilityModelRefForAgent({ cfg, agentId: "main", metadataSnapshot }),
).toBeUndefined();
});
});