From 750d97e8ad172c05e2bfe050d98ca12c1468198b Mon Sep 17 00:00:00 2001 From: chunterb <31895576+chunterb@users.noreply.github.com> Date: Tue, 22 Apr 2025 16:31:25 -0500 Subject: [PATCH] feat: add openai model info configuration (#551) In reference to [Issue 548](https://github.com/openai/codex/issues/548) - part 1. --- codex-cli/src/utils/model-info.ts | 198 ++++++++++++++++++++++++++++ codex-cli/src/utils/model-utils.ts | 9 +- codex-cli/tests/model-info.test.ts | 19 +++ codex-cli/tests/model-utils.test.ts | 78 +++++++++++ 4 files changed, 301 insertions(+), 3 deletions(-) create mode 100644 codex-cli/src/utils/model-info.ts create mode 100644 codex-cli/tests/model-info.test.ts create mode 100644 codex-cli/tests/model-utils.test.ts diff --git a/codex-cli/src/utils/model-info.ts b/codex-cli/src/utils/model-info.ts new file mode 100644 index 00000000..bbe0cb36 --- /dev/null +++ b/codex-cli/src/utils/model-info.ts @@ -0,0 +1,198 @@ +export type ModelInfo = { + /** The human-readable label for this model */ + label: string; + /** The max context window size for this model */ + maxContextLength: number; +}; + +export type SupportedModelId = keyof typeof openAiModelInfo; +export const openAiModelInfo = { + "o1-pro-2025-03-19": { + label: "o1 Pro (2025-03-19)", + maxContextLength: 200000, + }, + "o3": { + label: "o3", + maxContextLength: 200000, + }, + "o3-2025-04-16": { + label: "o3 (2025-04-16)", + maxContextLength: 200000, + }, + "o4-mini": { + label: "o4 Mini", + maxContextLength: 200000, + }, + "gpt-4.1-nano": { + label: "GPT-4.1 Nano", + maxContextLength: 1000000, + }, + "gpt-4.1-nano-2025-04-14": { + label: "GPT-4.1 Nano (2025-04-14)", + maxContextLength: 1000000, + }, + "o4-mini-2025-04-16": { + label: "o4 Mini (2025-04-16)", + maxContextLength: 200000, + }, + "gpt-4": { + label: "GPT-4", + maxContextLength: 8192, + }, + "o1-preview-2024-09-12": { + label: "o1 Preview (2024-09-12)", + maxContextLength: 128000, + }, + "gpt-4.1-mini": { + label: "GPT-4.1 Mini", + maxContextLength: 1000000, + }, + "gpt-3.5-turbo-instruct-0914": { + label: "GPT-3.5 Turbo Instruct (0914)", + maxContextLength: 4096, + }, + "gpt-4o-mini-search-preview": { + label: "GPT-4o Mini Search Preview", + maxContextLength: 128000, + }, + "gpt-4.1-mini-2025-04-14": { + label: "GPT-4.1 Mini (2025-04-14)", + maxContextLength: 1000000, + }, + "chatgpt-4o-latest": { + label: "ChatGPT-4o Latest", + maxContextLength: 128000, + }, + "gpt-3.5-turbo-1106": { + label: "GPT-3.5 Turbo (1106)", + maxContextLength: 16385, + }, + "gpt-4o-search-preview": { + label: "GPT-4o Search Preview", + maxContextLength: 128000, + }, + "gpt-4-turbo": { + label: "GPT-4 Turbo", + maxContextLength: 128000, + }, + "gpt-4o-realtime-preview-2024-12-17": { + label: "GPT-4o Realtime Preview (2024-12-17)", + maxContextLength: 128000, + }, + "gpt-3.5-turbo-instruct": { + label: "GPT-3.5 Turbo Instruct", + maxContextLength: 4096, + }, + "gpt-3.5-turbo": { + label: "GPT-3.5 Turbo", + maxContextLength: 16385, + }, + "gpt-4-turbo-preview": { + label: "GPT-4 Turbo Preview", + maxContextLength: 128000, + }, + "gpt-4o-mini-search-preview-2025-03-11": { + label: "GPT-4o Mini Search Preview (2025-03-11)", + maxContextLength: 128000, + }, + "gpt-4-0125-preview": { + label: "GPT-4 (0125) Preview", + maxContextLength: 128000, + }, + "gpt-4o-2024-11-20": { + label: "GPT-4o (2024-11-20)", + maxContextLength: 128000, + }, + "o3-mini": { + label: "o3 Mini", + maxContextLength: 200000, + }, + "gpt-4o-2024-05-13": { + label: "GPT-4o (2024-05-13)", + maxContextLength: 128000, + }, + "gpt-4-turbo-2024-04-09": { + label: "GPT-4 Turbo (2024-04-09)", + maxContextLength: 128000, + }, + "gpt-3.5-turbo-16k": { + label: "GPT-3.5 Turbo 16k", + maxContextLength: 16385, + }, + "o3-mini-2025-01-31": { + label: "o3 Mini (2025-01-31)", + maxContextLength: 200000, + }, + "o1-preview": { + label: "o1 Preview", + maxContextLength: 128000, + }, + "o1-2024-12-17": { + label: "o1 (2024-12-17)", + maxContextLength: 128000, + }, + "gpt-4-0613": { + label: "GPT-4 (0613)", + maxContextLength: 8192, + }, + "o1": { + label: "o1", + maxContextLength: 128000, + }, + "o1-pro": { + label: "o1 Pro", + maxContextLength: 200000, + }, + "gpt-4.5-preview": { + label: "GPT-4.5 Preview", + maxContextLength: 128000, + }, + "gpt-4.5-preview-2025-02-27": { + label: "GPT-4.5 Preview (2025-02-27)", + maxContextLength: 128000, + }, + "gpt-4o-search-preview-2025-03-11": { + label: "GPT-4o Search Preview (2025-03-11)", + maxContextLength: 128000, + }, + "gpt-4o": { + label: "GPT-4o", + maxContextLength: 128000, + }, + "gpt-4o-mini": { + label: "GPT-4o Mini", + maxContextLength: 128000, + }, + "gpt-4o-2024-08-06": { + label: "GPT-4o (2024-08-06)", + maxContextLength: 128000, + }, + "gpt-4.1": { + label: "GPT-4.1", + maxContextLength: 1000000, + }, + "gpt-4.1-2025-04-14": { + label: "GPT-4.1 (2025-04-14)", + maxContextLength: 1000000, + }, + "gpt-4o-mini-2024-07-18": { + label: "GPT-4o Mini (2024-07-18)", + maxContextLength: 128000, + }, + "o1-mini": { + label: "o1 Mini", + maxContextLength: 128000, + }, + "gpt-3.5-turbo-0125": { + label: "GPT-3.5 Turbo (0125)", + maxContextLength: 16385, + }, + "o1-mini-2024-09-12": { + label: "o1 Mini (2024-09-12)", + maxContextLength: 128000, + }, + "gpt-4-1106-preview": { + label: "GPT-4 (1106) Preview", + maxContextLength: 128000, + }, +} as const satisfies Record; diff --git a/codex-cli/src/utils/model-utils.ts b/codex-cli/src/utils/model-utils.ts index 774371ed..5670fc44 100644 --- a/codex-cli/src/utils/model-utils.ts +++ b/codex-cli/src/utils/model-utils.ts @@ -2,6 +2,7 @@ import type { ResponseItem } from "openai/resources/responses/responses.mjs"; import { approximateTokensUsed } from "./approximate-tokens-used.js"; import { getBaseUrl, getApiKey } from "./config"; +import { type SupportedModelId, openAiModelInfo } from "./model-info.js"; import OpenAI from "openai"; const MODEL_LIST_TIMEOUT_MS = 2_000; // 2 seconds @@ -89,10 +90,12 @@ export async function isModelSupportedForResponses( } /** Returns the maximum context length (in tokens) for a given model. */ -function maxTokensForModel(model: string): number { - // TODO: These numbers are best‑effort guesses and provide a basis for UI percentages. They - // should be provider & model specific instead of being wild guesses. +export function maxTokensForModel(model: string): number { + if (model in openAiModelInfo) { + return openAiModelInfo[model as SupportedModelId].maxContextLength; + } + // fallback to heuristics for models not in the registry const lower = model.toLowerCase(); if (lower.includes("32k")) { return 32000; diff --git a/codex-cli/tests/model-info.test.ts b/codex-cli/tests/model-info.test.ts new file mode 100644 index 00000000..9626744d --- /dev/null +++ b/codex-cli/tests/model-info.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, test } from "vitest"; +import { openAiModelInfo } from "../src/utils/model-info"; + +describe("Model Info", () => { + test("supportedModelInfo contains expected models", () => { + expect(openAiModelInfo).toHaveProperty("gpt-4o"); + expect(openAiModelInfo).toHaveProperty("gpt-4.1"); + expect(openAiModelInfo).toHaveProperty("o3"); + }); + + test("model info entries have required properties", () => { + Object.entries(openAiModelInfo).forEach(([_, info]) => { + expect(info).toHaveProperty("label"); + expect(info).toHaveProperty("maxContextLength"); + expect(typeof info.label).toBe("string"); + expect(typeof info.maxContextLength).toBe("number"); + }); + }); +}); diff --git a/codex-cli/tests/model-utils.test.ts b/codex-cli/tests/model-utils.test.ts new file mode 100644 index 00000000..6dd52382 --- /dev/null +++ b/codex-cli/tests/model-utils.test.ts @@ -0,0 +1,78 @@ +import { describe, test, expect } from "vitest"; +import { + calculateContextPercentRemaining, + maxTokensForModel, +} from "../src/utils/model-utils"; +import { openAiModelInfo } from "../src/utils/model-info"; +import type { ResponseItem } from "openai/resources/responses/responses.mjs"; + +describe("Model Utils", () => { + describe("openAiModelInfo", () => { + test("model info entries have required properties", () => { + Object.entries(openAiModelInfo).forEach(([_, info]) => { + expect(info).toHaveProperty("label"); + expect(info).toHaveProperty("maxContextLength"); + expect(typeof info.label).toBe("string"); + expect(typeof info.maxContextLength).toBe("number"); + }); + }); + }); + + describe("maxTokensForModel", () => { + test("returns correct token limit for known models", () => { + const knownModel = "gpt-4o"; + const expectedTokens = openAiModelInfo[knownModel].maxContextLength; + expect(maxTokensForModel(knownModel)).toBe(expectedTokens); + }); + + test("handles models with size indicators in their names", () => { + expect(maxTokensForModel("some-model-32k")).toBe(32000); + expect(maxTokensForModel("some-model-16k")).toBe(16000); + expect(maxTokensForModel("some-model-8k")).toBe(8000); + expect(maxTokensForModel("some-model-4k")).toBe(4000); + }); + + test("defaults to 128k for unknown models not in the registry", () => { + expect(maxTokensForModel("completely-unknown-model")).toBe(128000); + }); + }); + + describe("calculateContextPercentRemaining", () => { + test("returns 100% for empty items", () => { + const result = calculateContextPercentRemaining([], "gpt-4o"); + expect(result).toBe(100); + }); + + test("calculates percentage correctly for non-empty items", () => { + const mockItems: Array = [ + { + id: "test-id", + type: "message", + role: "user", + status: "completed", + content: [ + { + type: "input_text", + text: "A".repeat( + openAiModelInfo["gpt-4o"].maxContextLength * 0.25 * 4, + ), + }, + ], + } as ResponseItem, + ]; + + const result = calculateContextPercentRemaining(mockItems, "gpt-4o"); + expect(result).toBeCloseTo(75, 0); + }); + + test("handles models that are not in the registry", () => { + const mockItems: Array = []; + + const result = calculateContextPercentRemaining( + mockItems, + "unknown-model", + ); + expect(result).toBe(100); + }); + }); +});