diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 0e7cf378..e4b782e0 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -1,10 +1,10 @@ import * as acp from "@agentclientprotocol/sdk"; +import {type ModelInfo, RequestError, type SessionModelState} from "@agentclientprotocol/sdk"; import {CodexEventHandler} from "./CodexEventHandler"; import {CodexApprovalHandler} from "./CodexApprovalHandler"; import {CodexAuthMethods, type CodexAuthRequest} from "./CodexAuthMethod"; -import {type ModelInfo, RequestError, type SessionModelState} from "@agentclientprotocol/sdk"; import {CodexAcpClient, type SessionMetadata} from "./CodexAcpClient"; -import type {Model} from "./app-server/v2"; +import type {Account, Model, RateLimitSnapshot} from "./app-server/v2"; import type {ReasoningEffort} from "./app-server"; import {ModelId} from "./ModelId"; import {AgentMode} from "./AgentMode"; @@ -12,8 +12,13 @@ import type {TokenCount} from "./TokenCount"; import {CodexCommands} from "./CodexCommands"; import type {QuotaMeta} from "./QuotaMeta"; import {logger} from "./Logger"; -import type {RateLimitSnapshot, Account} from "./app-server/v2"; +const ALLOWED_MODEL_IDS = new Set([ + "gpt-5.2", + "gpt-5.2-codex", + "gpt-5.1-codex-max", + "gpt-5.1-codex-mini", +]); export interface SessionState { sessionMetadata: SessionMetadata; @@ -253,13 +258,15 @@ export class CodexAcpServer implements acp.Agent { } private buildAvailableModels(models: Model[]): ModelInfo[] { - return models.flatMap((model) => - model.supportedReasoningEfforts.map((effort) => ({ - modelId: ModelId.fromComponents(model, effort.reasoningEffort).toString(), - name: `${model.displayName} (${effort.reasoningEffort})`, - description: `${model.description} ${effort.description}`, - })) - ); + return models + .filter((model) => ALLOWED_MODEL_IDS.has(model.id)) + .flatMap((model) => + model.supportedReasoningEfforts.map((effort) => ({ + modelId: ModelId.fromComponents(model, effort.reasoningEffort).toString(), + name: `${model.displayName} (${effort.reasoningEffort})`, + description: `${model.description} ${effort.description}`, + })) + ); } getSessionState(sessionId: string): SessionState { diff --git a/src/__tests__/CodexACPAgent/data/model-filtering.json b/src/__tests__/CodexACPAgent/data/model-filtering.json new file mode 100644 index 00000000..a650f1fd --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/model-filtering.json @@ -0,0 +1,17 @@ +[ + { + "modelId": "gpt-5.2[medium]", + "name": "GPT-5.2 (medium)", + "description": "Allowed by id. Default effort." + }, + { + "modelId": "gpt-5.2[low]", + "name": "GPT-5.2 (low)", + "description": "Allowed by id. Fast effort." + }, + { + "modelId": "gpt-5.1-codex-mini[medium]", + "name": "Other (medium)", + "description": "Allowed by id. Default effort." + } +] \ No newline at end of file diff --git a/src/__tests__/CodexACPAgent/model-filtering.test.ts b/src/__tests__/CodexACPAgent/model-filtering.test.ts new file mode 100644 index 00000000..5a00ace3 --- /dev/null +++ b/src/__tests__/CodexACPAgent/model-filtering.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it, vi } from "vitest"; +import type { Model, ReasoningEffortOption } from "../../app-server/v2"; +import { AgentMode } from "../../AgentMode"; +import { createCodexMockTestFixture } from "../acp-test-utils"; + +describe("Model filtering", () => { + it("filters available models by id allowlist", async () => { + const fixture = createCodexMockTestFixture(); + const codexAcpAgent = fixture.getCodexAcpAgent(); + const codexAcpClient = fixture.getCodexAcpClient(); + + const defaultEffort: ReasoningEffortOption = {reasoningEffort: "medium", description: "Default effort."}; + const fastEffort: ReasoningEffortOption = {reasoningEffort: "low", description: "Fast effort."}; + const efforts: ReasoningEffortOption[] = [defaultEffort, fastEffort]; + + const models: Model[] = [ + { + id: "gpt-5.2", + model: "gpt-5.2-model-field", + displayName: "GPT-5.2", + description: "Allowed by id.", + supportedReasoningEfforts: efforts, + defaultReasoningEffort: "medium", + isDefault: false, + }, + { + id: "other-id", + model: "gpt-5.2", + displayName: "gpt-5.2", + description: "Looks allowed but id is not.", + supportedReasoningEfforts: [defaultEffort], + defaultReasoningEffort: "medium", + isDefault: false, + }, + { + id: "gpt-5.1-codex-mini", + model: "other-model", + displayName: "Other", + description: "Allowed by id.", + supportedReasoningEfforts: [defaultEffort], + defaultReasoningEffort: "medium", + isDefault: false, + }, + { + id: "gpt-4o", + model: "gpt-4o", + displayName: "gpt-4o", + description: "Not allowed.", + supportedReasoningEfforts: [defaultEffort], + defaultReasoningEffort: "medium", + isDefault: false, + }, + ]; + + vi.spyOn(codexAcpClient, "authRequired").mockResolvedValue(false); + vi.spyOn(codexAcpClient, "newSession").mockResolvedValue({ + sessionId: "session-id", + currentModelId: "gpt-5.2[medium]", + models, + agentMode: AgentMode.getInitialAgentMode(), + }); + vi.spyOn(codexAcpClient, "getAccount").mockResolvedValue({account: null, requiresOpenaiAuth: false}); + + const newSessionResponse = await codexAcpAgent.newSession({ cwd: "", mcpServers: [] }); + const sessionModels = newSessionResponse.models; + const availableModels = sessionModels?.availableModels; + + await expect(JSON.stringify(availableModels, null, 2)).toMatchFileSnapshot( + "data/model-filtering.json" + ); + }); +});