feat(openai): default new setups to GPT-5.6 (#103581)

* feat(openai): default fresh setup to GPT-5.6

* test(crestodian): expect GPT-5.6 Codex defaults

* test(crestodian): expect GPT-5.6 bootstrap default
This commit is contained in:
Peter Steinberger
2026-07-10 10:22:58 +01:00
committed by GitHub
parent fc60a8e66b
commit ab5d143d59
39 changed files with 652 additions and 179 deletions
+8 -2
View File
@@ -28,7 +28,7 @@ const HIGH_SIGNAL_LIVE_MODEL_PRIORITY = [
"deepseek/deepseek-v4-flash",
"deepseek/deepseek-v4-pro",
"minimax/minimax-m3",
"openai/gpt-5.5",
"openai/gpt-5.6",
"openrouter/openai/gpt-5.2-chat",
"openrouter/minimax/minimax-m2.7",
"opencode-go/glm-5",
@@ -56,6 +56,12 @@ export const DEFAULT_HIGH_SIGNAL_LIVE_MODEL_LIMIT = HIGH_SIGNAL_LIVE_MODEL_PRIOR
/** Default cap for the small-model live smoke lane. */
export const DEFAULT_SMALL_LIVE_MODEL_LIMIT = SMALL_LIVE_MODEL_PRIORITY.length;
const DEFAULT_HIGH_SIGNAL_LIVE_EXCLUDED_PROVIDERS = new Set(["codex", "codex-cli"]);
const DIRECT_OPENAI_HIGH_SIGNAL_LIVE_MODEL_IDS = new Set([
"gpt-5.6",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
]);
const CURATED_ONLY_HIGH_SIGNAL_LIVE_PROVIDERS = new Set([
"fireworks",
"google",
@@ -157,7 +163,7 @@ function isUnsupportedOpenAiLiveModelRef(provider: string, id: string): boolean
}
const modelName = normalizeLowercaseStringOrEmpty(id).split("/").pop() ?? "";
if (provider === "openai") {
return modelName !== "gpt-5.5";
return !DIRECT_OPENAI_HIGH_SIGNAL_LIVE_MODEL_IDS.has(modelName);
}
return !modelName.startsWith("gpt-5.2");
}
+18 -1
View File
@@ -4,7 +4,10 @@
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveVisibleModelCatalog } from "./model-catalog-visibility.js";
import {
isCodexRoutableOpenAIPlatformCatalogEntry,
resolveVisibleModelCatalog,
} from "./model-catalog-visibility.js";
import type { ModelCatalogEntry } from "./model-catalog.types.js";
const normalizeProviderModelIdWithRuntimeMock = vi.hoisted(() => vi.fn());
@@ -19,6 +22,20 @@ describe("resolveVisibleModelCatalog", () => {
normalizeProviderModelIdWithRuntimeMock.mockReset();
});
it("recognizes exact GPT-5.6 Codex ids without treating the API alias as routable", () => {
const entry = (id: string): ModelCatalogEntry => ({
provider: "openai",
id,
name: id,
api: "openai-responses",
});
expect(isCodexRoutableOpenAIPlatformCatalogEntry(entry("gpt-5.6"))).toBe(false);
expect(isCodexRoutableOpenAIPlatformCatalogEntry(entry("gpt-5.6-sol"))).toBe(true);
expect(isCodexRoutableOpenAIPlatformCatalogEntry(entry("gpt-5.6-terra"))).toBe(true);
expect(isCodexRoutableOpenAIPlatformCatalogEntry(entry("gpt-5.6-luna"))).toBe(true);
});
it("can use static auth checks for gateway read-only model lists", async () => {
const authChecker = vi.fn((provider: string) => provider === "openai");
const catalog: ModelCatalogEntry[] = [
+3
View File
@@ -20,6 +20,9 @@ type ProviderAuthChecker = (provider: string, modelApi?: string) => boolean | Pr
const OPENAI_PROVIDER_ID = "openai";
const OPENAI_CODEX_RESPONSES_API = "openai-chatgpt-responses";
const OPENAI_CODEX_ROUTABLE_MODEL_IDS = new Set([
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
"gpt-5.5",
"gpt-5.5-pro",
"gpt-5.4",
+20 -6
View File
@@ -393,9 +393,18 @@ describe("isModernModelRef", () => {
it("includes plugin-advertised modern models", () => {
providerRuntimeMocks.resolveProviderModernModelRef.mockImplementation(({ provider, context }) =>
provider === "openai" &&
["gpt-5.5", "gpt-5.5-pro", "gpt-5.4", "gpt-5.4-pro", "gpt-5.4-mini", "gpt-5.4-nano"].includes(
context.modelId,
)
[
"gpt-5.6",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
"gpt-5.5",
"gpt-5.5-pro",
"gpt-5.4",
"gpt-5.4-pro",
"gpt-5.4-mini",
"gpt-5.4-nano",
].includes(context.modelId)
? true
: provider === "openai" &&
["gpt-5.5", "gpt-5.5-pro", "gpt-5.4", "gpt-5.4-pro", "gpt-5.4-mini"].includes(
@@ -409,6 +418,8 @@ describe("isModernModelRef", () => {
: undefined,
);
expect(isModernModelRef({ provider: "openai", id: "gpt-5.6" })).toBe(true);
expect(isModernModelRef({ provider: "openai", id: "gpt-5.6-sol" })).toBe(true);
expect(isModernModelRef({ provider: "openai", id: "gpt-5.5" })).toBe(true);
expect(isModernModelRef({ provider: "openai", id: "gpt-5.5-pro" })).toBe(true);
expect(isModernModelRef({ provider: "openai", id: "gpt-5.4" })).toBe(true);
@@ -495,7 +506,7 @@ describe("isHighSignalLiveModelRef", () => {
);
});
it("keeps only the current direct OpenAI-family model in the default live matrix", () => {
it("keeps only the current direct OpenAI-family models in the default live matrix", () => {
providerRuntimeMocks.resolveProviderModernModelRef.mockReturnValue(true);
expect(isHighSignalLiveModelRef({ provider: "openrouter", id: "openai/gpt-3.5-turbo" })).toBe(
@@ -510,7 +521,10 @@ describe("isHighSignalLiveModelRef", () => {
expect(isHighSignalLiveModelRef({ provider: "openai", id: "gpt-5" })).toBe(false);
expect(isHighSignalLiveModelRef({ provider: "openai", id: "gpt-5.1" })).toBe(false);
expect(isHighSignalLiveModelRef({ provider: "openai", id: "gpt-5.4" })).toBe(false);
expect(isHighSignalLiveModelRef({ provider: "openai", id: "gpt-5.5" })).toBe(true);
expect(isHighSignalLiveModelRef({ provider: "openai", id: "gpt-5.5" })).toBe(false);
for (const id of ["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]) {
expect(isHighSignalLiveModelRef({ provider: "openai", id })).toBe(true);
}
expect(isHighSignalLiveModelRef({ provider: "openai", id: "gpt-5.2-codex" })).toBe(false);
expect(isHighSignalLiveModelRef({ provider: "openai", id: "gpt-5.2-chat-latest" })).toBe(false);
expect(isHighSignalLiveModelRef({ provider: "openrouter", id: "openai/gpt-5.1-chat" })).toBe(
@@ -680,7 +694,7 @@ describe("isPrioritizedHighSignalLiveModelRef", () => {
{ provider: "deepseek", id: "deepseek-v4-flash" },
{ provider: "deepseek", id: "deepseek-v4-pro" },
{ provider: "minimax", id: "minimax-m3" },
{ provider: "openai", id: "gpt-5.5" },
{ provider: "openai", id: "gpt-5.6" },
{ provider: "openrouter", id: "openai/gpt-5.2-chat" },
{ provider: "openrouter", id: "minimax/minimax-m2.7" },
{ provider: "opencode-go", id: "glm-5" },
+5
View File
@@ -17,6 +17,11 @@ function probeDeps(found: Record<string, boolean>) {
}
describe("detectInferenceBackends", () => {
it("uses route-specific GPT-5.6 defaults for direct API and Codex", () => {
expect(OPENAI_API_DEFAULT_MODEL_REF).toBe("openai/gpt-5.6");
expect(CODEX_APP_SERVER_DEFAULT_MODEL_REF).toBe("openai/gpt-5.6-sol");
});
it("returns nothing when no backend exists", async () => {
const candidates = await detectInferenceBackends({
env: {},
+2 -3
View File
@@ -5,7 +5,6 @@ import {
readGeminiCliCredentialsCached,
} from "../agents/cli-credentials.js";
// Inference backend detection shared by onboarding bootstrap and Crestodian setup.
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { probeLocalCommand, type LocalCommandProbe } from "../crestodian/probes.js";
@@ -16,10 +15,10 @@ import { probeLocalCommand, type LocalCommandProbe } from "../crestodian/probes.
* asking the user anything. The ladder order is a documented contract
* (docs/cli/crestodian.md "Setup bootstrap") — change docs when changing it.
*/
export const OPENAI_API_DEFAULT_MODEL_REF = `${DEFAULT_PROVIDER}/${DEFAULT_MODEL}`;
export const OPENAI_API_DEFAULT_MODEL_REF = "openai/gpt-5.6";
export const ANTHROPIC_API_DEFAULT_MODEL_REF = "anthropic/claude-opus-4-8";
export const CLAUDE_CLI_DEFAULT_MODEL_REF = "claude-cli/claude-opus-4-8";
export const CODEX_APP_SERVER_DEFAULT_MODEL_REF = OPENAI_API_DEFAULT_MODEL_REF;
export const CODEX_APP_SERVER_DEFAULT_MODEL_REF = "openai/gpt-5.6-sol";
export const GEMINI_CLI_DEFAULT_MODEL_REF = "google-gemini-cli/gemini-3.1-pro-preview";
export type InferenceBackendKind =
+4 -4
View File
@@ -211,7 +211,7 @@ describe("Crestodian assistant", () => {
const codexAppServerEntries = requireRecord(codexAppServerPlugins.entries);
const codexAppServerCodexEntry = requireRecord(codexAppServerEntries.codex);
expect(codexAppServerDefaults.workspace).toBe("/tmp/workspace");
expect(codexAppServerModel.primary).toBe("openai/gpt-5.5");
expect(codexAppServerModel.primary).toBe("openai/gpt-5.6-sol");
expect(codexAppServerCodexEntry.enabled).toBe(true);
});
@@ -246,12 +246,12 @@ describe("Crestodian assistant", () => {
}
expect(result.command).toBe("gateway status");
expect(result.reply).toBe("Codex planner online.");
expect(result.modelLabel).toBe("openai/gpt-5.5 via codex");
expect(result.modelLabel).toBe("openai/gpt-5.6-sol via codex");
expect(runEmbeddedAgent).toHaveBeenCalledTimes(1);
const firstEmbeddedCall = firstMockArg(runEmbeddedAgent);
expect(firstEmbeddedCall.provider).toBe("openai");
expect(firstEmbeddedCall.model).toBe("gpt-5.5");
expect(firstEmbeddedCall.model).toBe("gpt-5.6-sol");
expect(firstEmbeddedCall.agentHarnessId).toBe("codex");
expect(firstEmbeddedCall.disableTools).toBe(true);
expect(firstEmbeddedCall.toolsAllow).toEqual([]);
@@ -262,7 +262,7 @@ describe("Crestodian assistant", () => {
const embeddedPlugins = requireRecord(embeddedConfig.plugins);
const embeddedEntries = requireRecord(embeddedPlugins.entries);
const embeddedCodexEntry = requireRecord(embeddedEntries.codex);
expect(embeddedModel.primary).toBe("openai/gpt-5.5");
expect(embeddedModel.primary).toBe("openai/gpt-5.6-sol");
expect(embeddedCodexEntry.enabled).toBe(true);
});
+5 -5
View File
@@ -737,7 +737,7 @@ describe("parseCrestodianOperation", () => {
const { runtime, lines } = createCrestodianTestRuntime();
const applySetup = vi.fn(async () => ({
configPath: path.join(tempDir, "openclaw.json"),
lines: ["Workspace: /tmp/work", "Default model: openai/gpt-5.5"],
lines: ["Workspace: /tmp/work", "Default model: openai/gpt-5.6"],
}));
const plan = await executeCrestodianOperation(
@@ -748,7 +748,7 @@ describe("parseCrestodianOperation", () => {
expectRecordFields(plan as unknown as Record<string, unknown>, {
applied: false,
});
expect(lines.join("\n")).toContain("Model choice: openai/gpt-5.5 (OPENAI_API_KEY).");
expect(lines.join("\n")).toContain("Model choice: openai/gpt-5.6 (OPENAI_API_KEY).");
expect(applySetup).not.toHaveBeenCalled();
const result = await executeCrestodianOperation(
@@ -765,7 +765,7 @@ describe("parseCrestodianOperation", () => {
expect(lines.join("\n")).toContain("[crestodian] done: crestodian.setup");
expect(applySetup).toHaveBeenCalledWith({
workspace: "/tmp/work",
model: "openai/gpt-5.5",
model: "openai/gpt-5.6",
surface: "cli",
runtime,
});
@@ -775,12 +775,12 @@ describe("parseCrestodianOperation", () => {
audit,
{
operation: "crestodian.setup",
summary: "Bootstrapped setup with openai/gpt-5.5",
summary: "Bootstrapped setup with openai/gpt-5.6",
},
{
rescue: true,
workspace: "/tmp/work",
model: "openai/gpt-5.5",
model: "openai/gpt-5.6",
modelSource: "OPENAI_API_KEY",
},
);
+20 -10
View File
@@ -751,9 +751,10 @@ describe("activateSetupInference", () => {
async (params: { transform: (config: OpenClawConfig) => { nextConfig: OpenClawConfig } }) => {
const transformed = params.transform(persistedConfig).nextConfig;
const configuredRuntime =
transformed.agents?.defaults?.models?.["openai/gpt-5.5"]?.agentRuntime?.id ??
transformed.agents?.list?.find((agent) => agent.id === "ops")?.models?.["openai/gpt-5.5"]
?.agentRuntime?.id;
transformed.agents?.defaults?.models?.["openai/gpt-5.6-sol"]?.agentRuntime?.id ??
transformed.agents?.list?.find((agent) => agent.id === "ops")?.models?.[
"openai/gpt-5.6-sol"
]?.agentRuntime?.id;
events.push(
configuredRuntime === "codex" ? "persist-plugin-config" : "persist-plugin-install",
);
@@ -798,10 +799,13 @@ describe("activateSetupInference", () => {
expect.objectContaining({
id: "ops",
model: {
primary: "openai/gpt-5.5",
primary: "openai/gpt-5.6-sol",
fallbacks: ["google/gemini-3.1-pro-preview"],
},
models: { "openai/gpt-5.5": { agentRuntime: { id: "codex" } } },
models: {
"openai/gpt-5.5": { agentRuntime: { id: "openclaw" } },
"openai/gpt-5.6-sol": { agentRuntime: { id: "codex" } },
},
}),
],
},
@@ -811,7 +815,7 @@ describe("activateSetupInference", () => {
},
},
}),
model: "openai/gpt-5.5",
model: "openai/gpt-5.6-sol",
agentId: "ops",
}),
);
@@ -852,10 +856,13 @@ describe("activateSetupInference", () => {
expect.objectContaining({
id: "ops",
model: {
primary: "openai/gpt-5.5",
primary: "openai/gpt-5.6-sol",
fallbacks: ["google/gemini-3.1-pro-preview"],
},
models: { "openai/gpt-5.5": { agentRuntime: { id: "codex" } } },
models: {
"openai/gpt-5.5": { agentRuntime: { id: "openclaw" } },
"openai/gpt-5.6-sol": { agentRuntime: { id: "codex" } },
},
}),
],
},
@@ -879,10 +886,13 @@ describe("activateSetupInference", () => {
expect.objectContaining({
id: "ops",
model: {
primary: "openai/gpt-5.5",
primary: "openai/gpt-5.6-sol",
fallbacks: ["google/gemini-3.1-pro-preview"],
},
models: { "openai/gpt-5.5": { agentRuntime: { id: "codex" } } },
models: {
"openai/gpt-5.5": { agentRuntime: { id: "openclaw" } },
"openai/gpt-5.6-sol": { agentRuntime: { id: "codex" } },
},
}),
],
},
+14 -1
View File
@@ -2,6 +2,7 @@
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { normalizeUniqueStringEntries } from "@openclaw/normalization-core/string-normalization";
import { upsertAuthProfileWithLock } from "../agents/auth-profiles/profiles.js";
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { SecretInput } from "../config/types.secrets.js";
import { createLazyRuntimeSurface } from "../shared/lazy-runtime.js";
@@ -26,6 +27,7 @@ type ProviderApiKeyAuthMethodOptions = {
profileIds?: string[];
allowProfile?: boolean;
defaultModel?: string;
preserveExistingPrimary?: boolean;
expectedProviders?: string[];
metadata?: Record<string, string>;
noteMessage?: string;
@@ -74,6 +76,7 @@ async function applyApiKeyConfig(params: {
providerId: string;
profileIds: string[];
defaultModel?: string;
preserveExistingPrimary?: boolean;
applyConfig?: (cfg: OpenClawConfig) => OpenClawConfig;
}) {
const { applyAuthProfileConfig, applyPrimaryModel } = await loadProviderApiKeyAuthRuntime();
@@ -88,7 +91,16 @@ async function applyApiKeyConfig(params: {
if (params.applyConfig) {
next = params.applyConfig(next);
}
return params.defaultModel ? applyPrimaryModel(next, params.defaultModel) : next;
if (!params.defaultModel) {
return next;
}
if (
params.preserveExistingPrimary === true &&
resolveAgentModelPrimaryValue(next.agents?.defaults?.model) !== undefined
) {
return next;
}
return applyPrimaryModel(next, params.defaultModel);
}
/** Creates a provider auth method that captures, stores, and configures API-key credentials. */
@@ -204,6 +216,7 @@ export function createProviderApiKeyAuthMethod(
providerId: params.providerId,
profileIds,
defaultModel: params.defaultModel,
preserveExistingPrimary: params.preserveExistingPrimary,
applyConfig: params.applyConfig,
});
},
@@ -31,6 +31,28 @@ describe("applyProviderAuthConfigPatch", () => {
expect(next.agents?.defaults?.model).toEqual(base.agents.defaults.model);
});
it("keeps configured primary and fallback refs in a newly introduced allowlist", () => {
const next = applyProviderAuthConfigPatch(
{
agents: {
defaults: {
model: {
primary: "openai/gpt-5.5",
fallbacks: ["anthropic/claude-opus-4-6"],
},
},
},
},
{ agents: { defaults: { models: { "openai/gpt-5.6-sol": {} } } } },
);
expect(next.agents?.defaults?.models).toEqual({
"openai/gpt-5.6-sol": {},
"openai/gpt-5.5": {},
"anthropic/claude-opus-4-6": {},
});
});
it("replaces the allowlist only when replaceDefaultModels is set", () => {
const patch = {
agents: {
@@ -176,6 +198,7 @@ describe("applyProviderAuthConfigPatch", () => {
alias: "gemini",
params: { thinking: "high", maxTokens: 12_000 },
},
"openai/gpt-5.5": {},
});
});
@@ -327,6 +350,10 @@ describe("applyDefaultModel", () => {
expect(next.agents?.defaults?.model).toEqual({
primary: "anthropic/claude-opus-4-6",
});
expect(next.agents?.defaults?.models).toEqual({
"openrouter/auto": {},
"anthropic/claude-opus-4-6": {},
});
});
it("normalizes a preserved retired Google Gemini primary", () => {
@@ -363,6 +390,11 @@ describe("applyDefaultModel", () => {
primary: "anthropic/claude-opus-4-6",
fallbacks: ["openai/gpt-5.4"],
});
expect(next.agents?.defaults?.models).toEqual({
"openrouter/auto": {},
"anthropic/claude-opus-4-6": {},
"openai/gpt-5.4": {},
});
});
it("adds the model to the allowlist", () => {
+35 -3
View File
@@ -276,6 +276,38 @@ function normalizeConfigModelRefsForWrite(
};
}
/** Keep a restrictive model allowlist consistent with the configured primary and fallbacks. */
function ensureConfiguredDefaultModelsAllowed(cfg: OpenClawConfig): OpenClawConfig {
const defaults = cfg.agents?.defaults;
if (!defaults?.models) {
return cfg;
}
const model = defaults.model;
const refs = [
typeof model === "string" ? model : model?.primary,
...(typeof model === "object" ? (model.fallbacks ?? []) : []),
].filter((ref): ref is string => typeof ref === "string" && ref.trim().length > 0);
const models = normalizeAgentModelMapForConfig(defaults.models);
let changed = false;
for (const ref of refs) {
const normalizedRef = normalizeAgentModelRefForConfig(ref);
if (!models[normalizedRef]) {
models[normalizedRef] = {};
changed = true;
}
}
if (!changed) {
return cfg;
}
return {
...cfg,
agents: {
...cfg.agents,
defaults: { ...defaults, models },
},
};
}
export function applyProviderAuthConfigPatch(
cfg: OpenClawConfig,
patch: unknown,
@@ -291,7 +323,7 @@ export function applyProviderAuthConfigPatch(
providerConfigNormalizer,
);
if (!options?.replaceDefaultModels || !isPlainRecord(patch)) {
return merged;
return ensureConfiguredDefaultModelsAllowed(merged);
}
const patchModels = (patch.agents as { defaults?: { models?: unknown } } | undefined)?.defaults
@@ -369,7 +401,7 @@ export function applyDefaultModel(
normalizeAgentModelRefForConfig(fallback),
)
: undefined;
return {
return ensureConfiguredDefaultModelsAllowed({
...cfg,
agents: {
...cfg.agents,
@@ -385,5 +417,5 @@ export function applyDefaultModel(
},
},
},
};
});
}