fix(models): expose codex runtime context caps

This commit is contained in:
Peter Steinberger
2026-04-25 07:37:56 +01:00
parent 3d554aefdf
commit 2ff7eb36cf
18 changed files with 240 additions and 46 deletions

View File

@@ -91,6 +91,7 @@ Docs: https://docs.openclaw.ai
- Feishu: suppress duplicate final card delivery when idle closes a streaming card before the final payload arrives. (#68491) Thanks @MoerAI.
- Signal: preserve sender attachment filenames and resolve missing MIME types from those filenames, so Linux `signal-cli` voice notes without `contentType` still enter audio transcription. Fixes #48614. Thanks @mindfury.
- Telegram/agents: suppress the phantom "Agent couldn't generate a response" fallback after a reply was already committed through the messaging tool. (#70623) Thanks @chinar-amrutkar.
- Models/CLI: show provider runtime `contextTokens` beside native `contextWindow` in `openclaw models list`, and align `openai-codex/gpt-5.5` with Codex's 272K runtime cap plus 400K native window. Fixes #71403.
- Dashboard/security: avoid writing tokenized Control UI URLs or SSH hints to runtime logs, keeping gateway bearer fragments out of console-captured logs readable through `logs.tail`. (#70029) Thanks @Ziy1-Tan.
- Providers/OpenRouter: treat DeepSeek refs as cache-TTL eligible without injecting Anthropic cache-control markers, aligning context pruning with OpenRouter-managed prompt caching. (#51983) Thanks @QuinnH496.
- Control UI/browser: defer temp-dir access-mode constants until Node-only temp-dir resolution runs, preventing browser bundles from crashing when `node:fs` constants are stubbed. (#48930) Thanks @Valentinws.

View File

@@ -50,6 +50,10 @@ Notes:
- `models list --all` includes bundled provider-owned static catalog rows even
when you have not authenticated with that provider yet. Those rows still show
as unavailable until matching auth is configured.
- `models list` keeps native model metadata and runtime caps distinct. In table
output, `Ctx` shows `contextTokens/contextWindow` when an effective runtime
cap differs from the native context window; JSON rows include `contextTokens`
when a provider exposes that cap.
- `models list --provider <id>` filters by provider id, such as `moonshot` or
`openai-codex`. It does not accept display labels from interactive provider
pickers, such as `Moonshot AI`.

View File

@@ -30,11 +30,9 @@ Reference for **LLM/model providers** (not chat channels like WhatsApp/Telegram)
`google-gemini-cli`, or `codex-cli` when you want a local CLI backend.
Legacy `claude-cli/*`, `google-gemini-cli/*`, and `codex-cli/*` refs migrate
back to canonical provider refs with the runtime recorded separately.
- GPT-5.5 is currently available through subscription/OAuth routes:
`openai-codex/gpt-5.5` in PI or `openai/gpt-5.5` with the Codex app-server
harness. The direct API-key route for `openai/gpt-5.5` is supported once
OpenAI enables GPT-5.5 on the public API; until then use API-enabled models
such as `openai/gpt-5.4` for `OPENAI_API_KEY` setups.
- GPT-5.5 is available through `openai-codex/gpt-5.5` in PI, the native
Codex app-server harness, and the public OpenAI API when the bundled PI
catalog exposes `openai/gpt-5.5` for your install.
## Plugin-owned provider behavior
@@ -73,10 +71,10 @@ OpenClaw ships with the piai catalog. These providers require **no**
- Provider: `openai`
- Auth: `OPENAI_API_KEY`
- Optional rotation: `OPENAI_API_KEYS`, `OPENAI_API_KEY_1`, `OPENAI_API_KEY_2`, plus `OPENCLAW_LIVE_OPENAI_KEY` (single override)
- Example models: `openai/gpt-5.4`, `openai/gpt-5.4-mini`
- GPT-5.5 direct API support is future-ready here once OpenAI exposes GPT-5.5 on the API
- Verify direct API availability with `openclaw models list --provider openai`
before using `openai/gpt-5.5` without the Codex app-server runtime
- Example models: `openai/gpt-5.5`, `openai/gpt-5.4`, `openai/gpt-5.4-mini`
- GPT-5.5 direct API support depends on the bundled PI catalog version for
your install; verify with `openclaw models list --provider openai` before
using `openai/gpt-5.5` without the Codex app-server runtime.
- CLI: `openclaw onboard --auth-choice openai-api-key`
- Default transport is `auto` (WebSocket-first, SSE fallback)
- Override per model via `agents.defaults.models["openai/<model>"].params.transport` (`"sse"`, `"websocket"`, or `"auto"`)
@@ -133,9 +131,9 @@ OpenClaw ships with the piai catalog. These providers require **no**
`User-Agent`) are only attached on native Codex traffic to
`chatgpt.com/backend-api`, not generic OpenAI-compatible proxies
- Shares the same `/fast` toggle and `params.fastMode` config as direct `openai/*`; OpenClaw maps that to `service_tier=priority`
- `openai-codex/gpt-5.5` keeps native `contextWindow = 1000000` and a default runtime `contextTokens = 272000`; override the runtime cap with `models.providers.openai-codex.models[].contextTokens`
- `openai-codex/gpt-5.5` uses the Codex catalog native `contextWindow = 400000` and default runtime `contextTokens = 272000`; override the runtime cap with `models.providers.openai-codex.models[].contextTokens`
- Policy note: OpenAI Codex OAuth is explicitly supported for external tools/workflows like OpenClaw.
- Current GPT-5.5 access uses this OAuth/subscription route until OpenAI enables GPT-5.5 on the public API.
- Use `openai-codex/gpt-5.5` when you want the Codex OAuth/subscription route; use `openai/gpt-5.5` when your API-key setup and local catalog expose the public API route.
```json5
{

View File

@@ -415,7 +415,7 @@ OpenClaw uses the built-in model catalog. Add custom providers via `models.provi
- `request.allowPrivateNetwork`: when `true`, allow HTTPS to `baseUrl` when DNS resolves to private, CGNAT, or similar ranges, via the provider HTTP fetch guard (operator opt-in for trusted self-hosted OpenAI-compatible endpoints). WebSocket uses the same `request` for headers/TLS but not that fetch SSRF gate. Default `false`.
- `models.providers.*.models`: explicit provider model catalog entries.
- `models.providers.*.models.*.contextWindow`: native model context window metadata.
- `models.providers.*.models.*.contextTokens`: optional runtime context cap. Use this when you want a smaller effective context budget than the model's native `contextWindow`.
- `models.providers.*.models.*.contextTokens`: optional runtime context cap. Use this when you want a smaller effective context budget than the model's native `contextWindow`; `openclaw models list` shows both values when they differ.
- `models.providers.*.models.*.compat.supportsDeveloperRole`: optional compatibility hint. For `api: "openai-completions"` with a non-empty non-native `baseUrl` (host not `api.openai.com`), OpenClaw forces this to `false` at runtime. Empty/omitted `baseUrl` keeps default OpenAI behavior.
- `models.providers.*.models.*.compat.requiresStringContent`: optional compatibility hint for string-only OpenAI-compatible chat endpoints. When `true`, OpenClaw flattens pure text `messages[].content` arrays into plain strings before sending the request.
- `plugins.entries.amazon-bedrock.config.discovery`: Bedrock auto-discovery settings root.

View File

@@ -333,7 +333,7 @@ describe("openai codex provider", () => {
});
});
it("uses Pi metadata for gpt-5.5 and local launch metadata for gpt-5.5-pro", () => {
it("keeps Pi cost metadata but applies Codex context metadata for gpt-5.5", () => {
const provider = buildOpenAICodexProviderPlugin();
const model = provider.resolveDynamicModel?.({
@@ -343,7 +343,7 @@ describe("openai codex provider", () => {
createCodexTemplate({
id: "gpt-5.5",
cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
contextWindow: 400_000,
contextWindow: 272_000,
}),
) as never,
});
@@ -358,6 +358,7 @@ describe("openai codex provider", () => {
api: "openai-codex-responses",
baseUrl: "https://chatgpt.com/backend-api",
contextWindow: 400_000,
contextTokens: 272_000,
maxTokens: 128_000,
cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
});
@@ -387,7 +388,7 @@ describe("openai codex provider", () => {
baseUrl: "https://chatgpt.com/backend-api/codex",
reasoning: true,
input: ["text", "image"],
contextWindow: 1_000_000,
contextWindow: 400_000,
contextTokens: 272_000,
maxTokens: 128_000,
});

View File

@@ -50,8 +50,8 @@ const OPENAI_CODEX_GPT_54_MODEL_ID = "gpt-5.4";
const OPENAI_CODEX_GPT_54_LEGACY_MODEL_ID = "gpt-5.4-codex";
const OPENAI_CODEX_GPT_54_PRO_MODEL_ID = "gpt-5.4-pro";
const OPENAI_CODEX_GPT_54_MINI_MODEL_ID = "gpt-5.4-mini";
const OPENAI_CODEX_GPT_55_NATIVE_CONTEXT_TOKENS = 1_000_000;
const OPENAI_CODEX_GPT_55_DEFAULT_CONTEXT_TOKENS = 272_000;
const OPENAI_CODEX_GPT_55_CODEX_CONTEXT_TOKENS = 400_000;
const OPENAI_CODEX_GPT_55_DEFAULT_RUNTIME_CONTEXT_TOKENS = 272_000;
const OPENAI_CODEX_GPT_55_PRO_NATIVE_CONTEXT_TOKENS = 1_000_000;
const OPENAI_CODEX_GPT_55_PRO_DEFAULT_CONTEXT_TOKENS = 272_000;
const OPENAI_CODEX_GPT_54_NATIVE_CONTEXT_TOKENS = 1_050_000;
@@ -188,7 +188,11 @@ function resolveCodexForwardCompatModel(ctx: ProviderResolveDynamicModelContext)
| ProviderRuntimeModel
| undefined;
return (
model ??
withDefaultCodexContextMetadata({
model,
contextWindow: OPENAI_CODEX_GPT_55_CODEX_CONTEXT_TOKENS,
contextTokens: OPENAI_CODEX_GPT_55_DEFAULT_RUNTIME_CONTEXT_TOKENS,
}) ??
normalizeModelCompat({
id: trimmedModelId,
name: trimmedModelId,
@@ -198,8 +202,8 @@ function resolveCodexForwardCompatModel(ctx: ProviderResolveDynamicModelContext)
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: OPENAI_CODEX_GPT_55_NATIVE_CONTEXT_TOKENS,
contextTokens: OPENAI_CODEX_GPT_55_DEFAULT_CONTEXT_TOKENS,
contextWindow: OPENAI_CODEX_GPT_55_CODEX_CONTEXT_TOKENS,
contextTokens: OPENAI_CODEX_GPT_55_DEFAULT_RUNTIME_CONTEXT_TOKENS,
maxTokens: OPENAI_CODEX_GPT_54_MAX_TOKENS,
} as ProviderRuntimeModel)
);
@@ -280,6 +284,27 @@ function resolveCodexForwardCompatModel(ctx: ProviderResolveDynamicModelContext)
);
}
function withDefaultCodexContextMetadata(params: {
model: ProviderRuntimeModel | undefined;
contextWindow: number;
contextTokens: number;
}): ProviderRuntimeModel | undefined {
if (!params.model) {
return undefined;
}
const contextTokens =
typeof params.model.contextTokens === "number"
? params.model.contextTokens
: typeof params.model.contextWindow === "number" && params.model.contextWindow > 0
? Math.min(params.contextTokens, params.model.contextWindow)
: params.contextTokens;
return {
...params.model,
contextWindow: params.contextWindow,
contextTokens,
};
}
async function refreshOpenAICodexOAuthCredential(cred: OAuthCredential) {
try {
const { refreshOpenAICodexToken } = await import("./openai-codex-provider.runtime.js");

View File

@@ -231,24 +231,37 @@ function buildDynamicModel(
case "openai-codex": {
const isLegacyGpt54Alias = lower === "gpt-5.4-codex";
if (lower === "gpt-5.5") {
return (
(params.modelRegistry.find("openai-codex", modelId) as ResolvedModelLike | null) ??
cloneTemplate(
undefined,
modelId,
{
provider: "openai-codex",
api: "openai-codex-responses",
baseUrl: OPENAI_CODEX_BASE_URL,
reasoning: true,
input: ["text", "image"],
cost: OPENROUTER_FALLBACK_COST,
contextWindow: 1_000_000,
contextTokens: 272_000,
maxTokens: 128_000,
},
{},
)
const model = params.modelRegistry.find(
"openai-codex",
modelId,
) as ResolvedModelLike | null;
if (model) {
const modelContextTokens = model.contextTokens;
const modelContextWindow = model.contextWindow;
const contextTokens =
typeof modelContextTokens === "number"
? modelContextTokens
: Math.min(
272_000,
typeof modelContextWindow === "number" ? modelContextWindow : 272_000,
);
return { ...model, contextWindow: 400_000, contextTokens };
}
return cloneTemplate(
undefined,
modelId,
{
provider: "openai-codex",
api: "openai-codex-responses",
baseUrl: OPENAI_CODEX_BASE_URL,
reasoning: true,
input: ["text", "image"],
cost: OPENROUTER_FALLBACK_COST,
contextWindow: 400_000,
contextTokens: 272_000,
maxTokens: 128_000,
},
{},
);
}
const template =

View File

@@ -58,6 +58,7 @@ export function buildOpenAICodexForwardCompatExpectation(
baseUrl: string;
} {
const isGpt54 = id === "gpt-5.4";
const isGpt55 = id === "gpt-5.5";
const isGpt54Mini = id === "gpt-5.4-mini";
const isSpark = id === "gpt-5.3-codex-spark";
return {
@@ -74,8 +75,8 @@ export function buildOpenAICodexForwardCompatExpectation(
: isGpt54Mini
? { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 }
: OPENAI_CODEX_TEMPLATE_MODEL.cost,
contextWindow: isGpt54 ? 1_050_000 : isSpark ? 128_000 : 272000,
...(isGpt54 ? { contextTokens: 272_000 } : {}),
contextWindow: isGpt54 ? 1_050_000 : isGpt55 ? 400_000 : isSpark ? 128_000 : 272000,
...(isGpt54 || isGpt55 ? { contextTokens: 272_000 } : {}),
maxTokens: 128000,
};
}

View File

@@ -1156,7 +1156,7 @@ describe("resolveModel", () => {
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
input: ["text", "image"],
contextWindow: 1_000_000,
contextWindow: 400_000,
contextTokens: 272_000,
maxTokens: 128_000,
});

View File

@@ -229,6 +229,7 @@ async function buildAllOpenAiCodexRows(opts: { supplementCatalog?: boolean } = {
const seenKeys = listRowsModule.appendDiscoveredRows({
rows: rows as never,
models: loaded.models as never,
modelRegistry: loaded.registry as never,
context: context as never,
});
if (opts.supplementCatalog !== false) {
@@ -576,6 +577,74 @@ describe("modelsListCommand forward-compat", () => {
]);
});
it("uses provider runtime metadata for discovered codex gpt-5.5 rows", async () => {
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
mocks.loadModelRegistry.mockResolvedValueOnce({
models: [
{
provider: "openai-codex",
id: "gpt-5.5",
name: "GPT-5.5",
api: "openai-codex-responses",
baseUrl: "https://chatgpt.com/backend-api",
input: ["text", "image"],
contextWindow: 272000,
maxTokens: 128000,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
},
],
availableKeys: new Set(["openai-codex/gpt-5.5"]),
registry: {
getAll: () => [
{
provider: "openai-codex",
id: "gpt-5.5",
name: "GPT-5.5",
api: "openai-codex-responses",
baseUrl: "https://chatgpt.com/backend-api",
input: ["text", "image"],
contextWindow: 272000,
maxTokens: 128000,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
},
],
},
});
mocks.resolveModelWithRegistry.mockImplementation(
({ provider, modelId }: { provider: string; modelId: string }) =>
provider === "openai-codex" && modelId === "gpt-5.5"
? {
provider: "openai-codex",
id: "gpt-5.5",
name: "GPT-5.5",
api: "openai-codex-responses",
baseUrl: "https://chatgpt.com/backend-api",
input: ["text", "image"],
contextWindow: 400000,
contextTokens: 272000,
maxTokens: 128000,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
}
: undefined,
);
const runtime = createRuntime();
await modelsListCommand(
{ all: true, provider: "openai-codex", json: true },
runtime as never,
);
expect(
lastPrintedRows<{ key: string; contextWindow: number; contextTokens?: number }>(),
).toEqual([
expect.objectContaining({
key: "openai-codex/gpt-5.5",
contextWindow: 400000,
contextTokens: 272000,
}),
]);
});
it("suppresses direct openai gpt-5.3-codex-spark rows in --all output", async () => {
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
const rows: unknown[] = [];

View File

@@ -15,6 +15,23 @@ const OPENROUTER_MODEL = {
} as const;
describe("toModelRow", () => {
it("keeps native context metadata and effective runtime context tokens distinct", () => {
const row = toModelRow({
model: {
...OPENROUTER_MODEL,
contextWindow: 400_000,
contextTokens: 272_000,
} as never,
key: "openrouter/openai/gpt-5.4",
tags: [],
});
expect(row).toMatchObject({
contextWindow: 400_000,
contextTokens: 272_000,
});
});
it("marks models available from auth profiles without loading model discovery", () => {
const authStore: AuthProfileStore = {
version: 1,

View File

@@ -11,6 +11,7 @@ export type ListRowModel = {
input: Array<"text" | "image">;
baseUrl?: string;
contextWindow?: number | null;
contextTokens?: number | null;
};
export type ModelAuthAvailabilityResolver = (params: {
@@ -97,6 +98,7 @@ export function toModelRow(params: {
name: model.name || model.id,
input,
contextWindow: model.contextWindow ?? null,
...(typeof model.contextTokens === "number" ? { contextTokens: model.contextTokens } : {}),
local,
available,
tags: Array.from(mergedTags),

View File

@@ -57,6 +57,7 @@ export async function appendAllModelRowSources(
appendDiscoveredRows({
rows: params.rows,
models: params.modelRegistry.getAll(),
modelRegistry: params.modelRegistry,
context: params.context,
});
}
@@ -66,6 +67,7 @@ export async function appendAllModelRowSources(
const seenKeys = appendDiscoveredRows({
rows: params.rows,
models: params.modelRegistry?.getAll() ?? [],
modelRegistry: params.modelRegistry,
context: params.context,
});

View File

@@ -130,6 +130,7 @@ function toConfiguredProviderListModel(params: {
baseUrl: params.model.baseUrl ?? params.providerConfig.baseUrl,
input: resolveConfiguredModelInput({ model: params.model }),
contextWindow: params.model.contextWindow ?? DEFAULT_CONTEXT_TOKENS,
contextTokens: params.model.contextTokens,
};
}
@@ -143,6 +144,7 @@ function shouldListConfiguredProviderModel(params: {
export function appendDiscoveredRows(params: {
rows: ModelRow[];
models: Model<Api>[];
modelRegistry?: ModelRegistry;
context: RowBuilderContext;
}): Set<string> {
const seenKeys = new Set<string>();
@@ -156,7 +158,26 @@ export function appendDiscoveredRows(params: {
for (const model of sorted) {
const key = modelKey(model.provider, model.id);
appendVisibleRow({ rows: params.rows, model, key, context: params.context, seenKeys });
const resolvedModel = params.modelRegistry
? resolveModelWithRegistry({
provider: model.provider,
modelId: model.id,
modelRegistry: params.modelRegistry,
cfg: params.context.cfg,
agentDir: params.context.agentDir,
})
: undefined;
const rowModel =
resolvedModel && modelKey(resolvedModel.provider, resolvedModel.id) === key
? resolvedModel
: model;
appendVisibleRow({
rows: params.rows,
model: rowModel,
key,
context: params.context,
seenKeys,
});
}
return seenKeys;

View File

@@ -0,0 +1,26 @@
import { describe, expect, it, vi } from "vitest";
import { printModelTable } from "./list.table.js";
import type { ModelRow } from "./list.types.js";
describe("printModelTable", () => {
it("prints effective and native context values when a runtime cap differs", () => {
const runtime = { log: vi.fn(), error: vi.fn() };
const rows: ModelRow[] = [
{
key: "openai-codex/gpt-5.5",
name: "GPT-5.5",
input: "text+image",
contextWindow: 400_000,
contextTokens: 272_000,
local: false,
available: true,
tags: [],
missing: false,
},
];
printModelTable(rows, runtime as never);
expect(runtime.log).toHaveBeenCalledWith(expect.stringContaining("266k/391k"));
});
});

View File

@@ -7,10 +7,22 @@ import { formatTokenK } from "./shared.js";
const MODEL_PAD = 42;
const INPUT_PAD = 10;
const CTX_PAD = 8;
const CTX_PAD = 11;
const LOCAL_PAD = 5;
const AUTH_PAD = 5;
function formatContextLabel(row: ModelRow): string {
if (
typeof row.contextTokens === "number" &&
Number.isFinite(row.contextTokens) &&
row.contextTokens > 0 &&
row.contextTokens !== row.contextWindow
) {
return `${formatTokenK(row.contextTokens)}/${formatTokenK(row.contextWindow)}`;
}
return formatTokenK(row.contextWindow);
}
export function printModelTable(
rows: ModelRow[],
runtime: RuntimeEnv,
@@ -45,7 +57,7 @@ export function printModelTable(
for (const row of rows) {
const keyLabel = pad(truncate(sanitizeTerminalText(row.key), MODEL_PAD), MODEL_PAD);
const inputLabel = pad(sanitizeTerminalText(row.input) || "-", INPUT_PAD);
const ctxLabel = pad(formatTokenK(row.contextWindow), CTX_PAD);
const ctxLabel = pad(formatContextLabel(row), CTX_PAD);
const localText = row.local === null ? "-" : row.local ? "yes" : "no";
const localLabel = pad(localText, LOCAL_PAD);
const authText = row.available === null ? "-" : row.available ? "yes" : "no";

View File

@@ -10,6 +10,7 @@ export type ModelRow = {
name: string;
input: string;
contextWindow: number | null;
contextTokens?: number;
local: boolean | null;
available: boolean | null;
tags: string[];

View File

@@ -563,7 +563,7 @@ export function describeOpenAIProviderRuntimeContract(load: ProviderRuntimeContr
});
});
it("uses Pi registry metadata for codex gpt-5.5 models", () => {
it("keeps Pi cost metadata but applies Codex context metadata for gpt-5.5 models", () => {
const provider = requireProviderContractProvider("openai-codex");
const model = provider.resolveDynamicModel?.({
provider: "openai-codex",
@@ -578,7 +578,7 @@ export function describeOpenAIProviderRuntimeContract(load: ProviderRuntimeContr
baseUrl: "https://chatgpt.com/backend-api",
input: ["text", "image"],
cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
contextWindow: 400_000,
contextWindow: 272_000,
maxTokens: 128_000,
})
: null,
@@ -590,6 +590,7 @@ export function describeOpenAIProviderRuntimeContract(load: ProviderRuntimeContr
provider: "openai-codex",
api: "openai-codex-responses",
contextWindow: 400_000,
contextTokens: 272_000,
maxTokens: 128_000,
});
});