fix(models): preserve Codex selection for catalog-seeded models (#126531)

* fix(models): keep catalog compat out of route overrides

* fix(models): preserve authored compat provenance

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
Hannes Rudolph
2026-08-19 22:54:57 -06:00
committed by GitHub
parent efc33f882e
commit 9441e3fe6e
6 changed files with 108 additions and 8 deletions
+44
View File
@@ -4,6 +4,10 @@ import type { Model } from "openclaw/plugin-sdk/llm";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createTempDirTracker } from "../../../test/helpers/temp-dir.js";
import type { OpenClawConfig } from "../../config/config.js";
import {
clearRuntimeConfigSnapshot,
setRuntimeConfigSnapshot,
} from "../../config/runtime-snapshot.js";
import { replaceSessionEntry } from "../../config/sessions/session-accessor.js";
import type { TranscriptEntryAnchor } from "../../config/sessions/transcript-entry-anchor.js";
import { OPENCLAW_EMBEDDED_CONTEXT_ENGINE_HOST } from "../../context-engine/host-compat.js";
@@ -226,6 +230,7 @@ beforeEach(async () => {
});
afterEach(() => {
clearRuntimeConfigSnapshot();
closeOpenClawAgentDatabasesForTest();
closeOpenClawStateDatabaseForTest();
trajectoryTempDirs.cleanup();
@@ -2036,6 +2041,45 @@ describe("selectAgentHarness", () => {
});
});
it("ignores catalog-seeded compatibility when selecting an official OpenAI route", () => {
const createConfig = (compat?: { supportsStore: boolean }) =>
({
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
models: [
{
id: "gpt-5.5",
name: "GPT-5.5",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
maxTokens: 8192,
...(compat ? { compat } : {}),
},
],
},
},
},
}) satisfies OpenClawConfig;
const sourceConfig = createConfig();
const runtimeConfig = createConfig({ supportsStore: false });
setRuntimeConfigSnapshot(runtimeConfig, sourceConfig);
expect(
buildAgentHarnessSupportContext({
provider: "openai",
modelId: "gpt-5.5",
requestedRuntime: "codex",
config: runtimeConfig,
}).modelProvider,
).toMatchObject({
requestTransportOverrides: "none",
runtimePolicy: { compatibleIds: ["openclaw", "codex"] },
});
});
it.each([
{
label: "default",
+5 -1
View File
@@ -5,6 +5,7 @@ import {
resolveMergedModelProviderModels,
resolveModelProviderRouteOverridePresence,
} from "../../config/model-provider-config.js";
import { projectConfigOntoRuntimeSourceSnapshot } from "../../config/runtime-source-projection.js";
import type { ModelApi } from "../../config/types.models.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type {
@@ -86,6 +87,9 @@ export function buildAgentHarnessSupportContext(params: {
providerOwnership?: HarnessProviderOwnership;
}): AgentHarnessSupportContext {
const providerConfig = resolveMergedModelProviderConfig(params.config, params.provider);
const authoredConfig = params.config
? projectConfigOntoRuntimeSourceSnapshot(params.config)
: undefined;
const modelId = params.modelId ? normalizeModelId(params.provider, params.modelId) : undefined;
const modelConfig = modelId
? resolveMergedModelProviderModels({
@@ -119,7 +123,7 @@ export function buildAgentHarnessSupportContext(params: {
requestTransportOverrides: resolveModelProviderRouteOverridePresence({
provider: params.provider,
modelId: params.modelId,
config: params.config,
authoredConfig,
canonicalizeModelId: (configuredModelId) =>
canonicalizeProviderModelId(params.provider, configuredModelId),
}),
+46
View File
@@ -1,6 +1,8 @@
// Verifies default config values and environment-sensitive overrides.
import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ProviderResolveModelRoutesContext } from "../plugin-sdk/provider-model-types.js";
import { resolveProviderModelRoutes } from "../plugins/provider-model-routes.js";
import {
DEFAULT_SUBAGENT_ARCHIVE_AFTER_MINUTES,
DEFAULT_SUBAGENT_MAX_CONCURRENT,
@@ -11,6 +13,8 @@ import {
applyContextPruningDefaults,
applyMessageDefaults,
} from "./defaults.js";
import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "./runtime-snapshot.js";
import type { OpenClawConfig } from "./types.openclaw.js";
const mocks = vi.hoisted(() => ({
applyProviderConfigDefaultsForConfig: vi.fn(),
@@ -32,6 +36,7 @@ describe("config defaults", () => {
});
afterEach(() => {
clearRuntimeConfigSnapshot();
vi.unstubAllEnvs();
});
@@ -158,6 +163,7 @@ describe("applyModelDefaults catalog seeding", () => {
maxTokens: 128_000,
cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 },
thinkingLevelMap: { off: "none" },
compat: { supportsStore: false },
},
],
},
@@ -243,6 +249,46 @@ describe("applyModelDefaults catalog seeding", () => {
expect(model.maxTokens).toBe(4_096);
});
it("keeps catalog-seeded compatibility out of authored route overrides", async () => {
const { applyModelDefaults } = await import("./defaults.js");
const sourceConfig: OpenClawConfig = {
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
models: [
{
id: "gpt-5.6-sol",
name: "GPT-5.6 Sol",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
maxTokens: 8192,
},
],
},
},
},
};
const runtimeConfig = applyModelDefaults(sourceConfig, { manifestRegistry: catalogRegistry });
setRuntimeConfigSnapshot(runtimeConfig, sourceConfig);
const resolveModelRoutes = vi.fn((_context: ProviderResolveModelRoutesContext) => ({
kind: "indeterminate" as const,
}));
resolveProviderModelRoutes({
provider: "openai",
modelId: "gpt-5.6-sol",
config: runtimeConfig,
env: {},
surface: { resolveModelRoutes },
});
expect(resolveModelRoutes.mock.calls[0]?.[0]).toMatchObject({
requestTransportOverrides: "none",
});
});
it("preserves catalog tiered pricing when flat cost fields are authored", async () => {
const { applyModelDefaults } = await import("./defaults.js");
const tieredRegistry = {
+3 -3
View File
@@ -68,14 +68,14 @@ describe("resolveModelProviderRouteOverridePresence", () => {
resolveModelProviderRouteOverridePresence({
provider: "openai",
modelId: "gpt-5.5",
config,
authoredConfig: config,
}),
).toBe("present");
expect(
resolveModelProviderRouteOverridePresence({
provider: "openai",
modelId: "gpt-5.5-empty",
config,
authoredConfig: config,
}),
).toBe("none");
});
@@ -85,7 +85,7 @@ describe("resolveModelProviderRouteOverridePresence", () => {
resolveModelProviderRouteOverridePresence({
provider: "openai",
modelId: "gpt-5.5",
config: {
authoredConfig: {
models: {
providers: {
openai: { baseUrl: "", timeoutSeconds: 90, models: [model("gpt-5.5")] },
+2 -2
View File
@@ -46,10 +46,10 @@ function hasNonEmptyRecord(value: unknown): boolean {
export function resolveModelProviderRouteOverridePresence(params: {
provider: string;
modelId?: string;
config?: OpenClawConfig;
authoredConfig?: OpenClawConfig;
canonicalizeModelId?: (modelId: string) => string;
}): ProviderRouteOverridePresence {
const providerConfig = resolveMergedModelProviderConfig(params.config, params.provider);
const providerConfig = resolveMergedModelProviderConfig(params.authoredConfig, params.provider);
if (!providerConfig) {
return "none";
}
+8 -2
View File
@@ -5,6 +5,7 @@ import {
resolveMergedModelProviderModels,
resolveModelProviderRouteOverridePresence,
} from "../config/model-provider-config.js";
import { projectConfigOntoRuntimeSourceSnapshot } from "../config/runtime-source-projection.js";
import type { ModelApi, ModelDefinitionConfig } from "../config/types.models.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type {
@@ -84,6 +85,11 @@ export function createProviderModelRoutesResolver(params: {
: params.surface;
const resolveModelRoutes = surface?.resolveModelRoutes;
const providerConfig = resolveMergedModelProviderConfig(params.config, provider);
// Runtime defaults copy catalog capabilities into configured model rows. Route
// eligibility must read the authored view or metadata looks like request behavior.
const authoredConfig = params.config
? projectConfigOntoRuntimeSourceSnapshot(params.config)
: undefined;
const configuredProvider = providerConfig
? { api: providerConfig.api, baseUrl: providerConfig.baseUrl }
: undefined;
@@ -105,7 +111,7 @@ export function createProviderModelRoutesResolver(params: {
? "present"
: resolveModelProviderRouteOverridePresence({
provider,
config: params.config,
authoredConfig,
});
const routeOverridePresenceByModel = new Map(
[...configuredModels.keys()].map(
@@ -117,7 +123,7 @@ export function createProviderModelRoutesResolver(params: {
: resolveModelProviderRouteOverridePresence({
provider,
modelId,
config: params.config,
authoredConfig,
canonicalizeModelId,
}),
] as const,