diff --git a/CHANGELOG.md b/CHANGELOG.md index 3447be3bc54..db79b8104e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/cli/models.md b/docs/cli/models.md index 0a1f7a66ed5..1fd734a505c 100644 --- a/docs/cli/models.md +++ b/docs/cli/models.md @@ -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 ` filters by provider id, such as `moonshot` or `openai-codex`. It does not accept display labels from interactive provider pickers, such as `Moonshot AI`. diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index bf233135cf9..0299ab687f6 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -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 pi‑ai 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/"].params.transport` (`"sse"`, `"websocket"`, or `"auto"`) @@ -133,9 +131,9 @@ OpenClaw ships with the pi‑ai 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 { diff --git a/docs/gateway/config-tools.md b/docs/gateway/config-tools.md index 8882c363c92..4795d1ef71e 100644 --- a/docs/gateway/config-tools.md +++ b/docs/gateway/config-tools.md @@ -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. diff --git a/extensions/openai/openai-codex-provider.test.ts b/extensions/openai/openai-codex-provider.test.ts index 406717258c0..732fe0f600c 100644 --- a/extensions/openai/openai-codex-provider.test.ts +++ b/extensions/openai/openai-codex-provider.test.ts @@ -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, }); diff --git a/extensions/openai/openai-codex-provider.ts b/extensions/openai/openai-codex-provider.ts index 032fee6d599..5ee57ab96a6 100644 --- a/extensions/openai/openai-codex-provider.ts +++ b/extensions/openai/openai-codex-provider.ts @@ -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"); diff --git a/src/agents/pi-embedded-runner/model.provider-runtime.test-support.ts b/src/agents/pi-embedded-runner/model.provider-runtime.test-support.ts index 4e3413856ae..18823f619c7 100644 --- a/src/agents/pi-embedded-runner/model.provider-runtime.test-support.ts +++ b/src/agents/pi-embedded-runner/model.provider-runtime.test-support.ts @@ -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 = diff --git a/src/agents/pi-embedded-runner/model.test-harness.ts b/src/agents/pi-embedded-runner/model.test-harness.ts index 0469d72650d..3105a10d4bb 100644 --- a/src/agents/pi-embedded-runner/model.test-harness.ts +++ b/src/agents/pi-embedded-runner/model.test-harness.ts @@ -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, }; } diff --git a/src/agents/pi-embedded-runner/model.test.ts b/src/agents/pi-embedded-runner/model.test.ts index 0e18e02615e..f4377edd46e 100644 --- a/src/agents/pi-embedded-runner/model.test.ts +++ b/src/agents/pi-embedded-runner/model.test.ts @@ -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, }); diff --git a/src/commands/models/list.list-command.forward-compat.test.ts b/src/commands/models/list.list-command.forward-compat.test.ts index d1d2d64b001..2c8f246f20c 100644 --- a/src/commands/models/list.list-command.forward-compat.test.ts +++ b/src/commands/models/list.list-command.forward-compat.test.ts @@ -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[] = []; diff --git a/src/commands/models/list.model-row.test.ts b/src/commands/models/list.model-row.test.ts index 26aa9f09fb0..df093030a2d 100644 --- a/src/commands/models/list.model-row.test.ts +++ b/src/commands/models/list.model-row.test.ts @@ -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, diff --git a/src/commands/models/list.model-row.ts b/src/commands/models/list.model-row.ts index 52e67d2f6bc..f02086d8847 100644 --- a/src/commands/models/list.model-row.ts +++ b/src/commands/models/list.model-row.ts @@ -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), diff --git a/src/commands/models/list.row-sources.ts b/src/commands/models/list.row-sources.ts index 8330aded170..39f5cd35f66 100644 --- a/src/commands/models/list.row-sources.ts +++ b/src/commands/models/list.row-sources.ts @@ -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, }); diff --git a/src/commands/models/list.rows.ts b/src/commands/models/list.rows.ts index 0c63358790d..902763065e8 100644 --- a/src/commands/models/list.rows.ts +++ b/src/commands/models/list.rows.ts @@ -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[]; + modelRegistry?: ModelRegistry; context: RowBuilderContext; }): Set { const seenKeys = new Set(); @@ -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; diff --git a/src/commands/models/list.table.test.ts b/src/commands/models/list.table.test.ts new file mode 100644 index 00000000000..5b70f4242e7 --- /dev/null +++ b/src/commands/models/list.table.test.ts @@ -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")); + }); +}); diff --git a/src/commands/models/list.table.ts b/src/commands/models/list.table.ts index 70249e18136..cb9cc968330 100644 --- a/src/commands/models/list.table.ts +++ b/src/commands/models/list.table.ts @@ -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"; diff --git a/src/commands/models/list.types.ts b/src/commands/models/list.types.ts index ba5c45893ef..060286a888d 100644 --- a/src/commands/models/list.types.ts +++ b/src/commands/models/list.types.ts @@ -10,6 +10,7 @@ export type ModelRow = { name: string; input: string; contextWindow: number | null; + contextTokens?: number; local: boolean | null; available: boolean | null; tags: string[]; diff --git a/test/helpers/plugins/provider-runtime-contract.ts b/test/helpers/plugins/provider-runtime-contract.ts index 20e43e0c22e..32b9f1a9f3a 100644 --- a/test/helpers/plugins/provider-runtime-contract.ts +++ b/test/helpers/plugins/provider-runtime-contract.ts @@ -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, }); });