From 81070b28f9d53d4f5c0eff8e3f188d45299a0b30 Mon Sep 17 00:00:00 2001 From: Richard Solomou Date: Sun, 19 Jul 2026 13:20:05 +0300 Subject: [PATCH 1/3] fix(codex): restore gateway models after reconnect Use the gateway model catalog when the native app-server returns stale model metadata. Generated-By: PostHog Code Task-Id: dfa9be76-95bf-4166-8adc-529483e3d525 --- packages/agent/src/adapters/acp-connection.ts | 1 + .../codex-app-server-agent.ts | 2 ++ .../codex-app-server/session-config.test.ts | 20 +++++++++++++ .../codex-app-server/session-config.ts | 29 ++++++++++++++++--- 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/packages/agent/src/adapters/acp-connection.ts b/packages/agent/src/adapters/acp-connection.ts index 3f76a774fb..61a0e97417 100644 --- a/packages/agent/src/adapters/acp-connection.ts +++ b/packages/agent/src/adapters/acp-connection.ts @@ -230,6 +230,7 @@ function createCodexConnection(config: AcpConnectionConfig): AcpConnection { }, model: codexOptions.model, reasoningEffort: codexOptions.reasoningEffort, + allowedModelIds: config.allowedModelIds, processCallbacks: config.processCallbacks, onStructuredOutput: config.onStructuredOutput, logger: config.logger?.child("CodexAppServerAgent"), diff --git a/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts b/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts index 8b1ce61b89..a18e379c4a 100644 --- a/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts +++ b/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts @@ -203,6 +203,7 @@ export interface CodexAppServerAgentOptions { processOptions: CodexAppServerProcessOptions; model?: string; reasoningEffort?: string; + allowedModelIds?: ReadonlySet; processCallbacks?: ProcessSpawnedCallback; logger?: Logger; onStructuredOutput?: (output: Record) => Promise; @@ -265,6 +266,7 @@ export class CodexAppServerAgent extends BaseAcpAgent { this.config = new SessionConfigState( options.model ?? DEFAULT_CODEX_MODEL, options.reasoningEffort, + options.allowedModelIds, ); this.onStructuredOutput = options.onStructuredOutput; this.developerInstructions = options.processOptions.developerInstructions; diff --git a/packages/agent/src/adapters/codex-app-server/session-config.test.ts b/packages/agent/src/adapters/codex-app-server/session-config.test.ts index d476904096..876d2f90cf 100644 --- a/packages/agent/src/adapters/codex-app-server/session-config.test.ts +++ b/packages/agent/src/adapters/codex-app-server/session-config.test.ts @@ -134,6 +134,26 @@ describe("SessionConfigState", () => { config.options.find((option) => option.category === "mode")?.currentValue, ).toBe("full-access"); }); + + it("uses gateway models when the app-server model list is stale", () => { + const config = new SessionConfigState( + "gpt-5.5", + undefined, + new Set(["gpt-5.5", "gpt-5.6-sol"]), + ); + + config.loadModels([ + { id: "gpt-5.5", model: "gpt-5.5", displayName: "GPT-5.5" }, + ]); + + const modelOption = config.options.find( + (option) => option.category === "model", + ); + expect(modelOption?.type === "select" ? modelOption.options : []).toEqual([ + { name: "GPT-5.5", value: "gpt-5.5" }, + { name: "gpt-5.6-sol", value: "gpt-5.6-sol" }, + ]); + }); }); describe("buildConfigOptions", () => { diff --git a/packages/agent/src/adapters/codex-app-server/session-config.ts b/packages/agent/src/adapters/codex-app-server/session-config.ts index 6070ca7e27..9a0ef2e13c 100644 --- a/packages/agent/src/adapters/codex-app-server/session-config.ts +++ b/packages/agent/src/adapters/codex-app-server/session-config.ts @@ -229,10 +229,16 @@ export class SessionConfigState { private models: Array<{ id: string; name: string }> = []; private efforts: string[] = []; private _options: SessionConfigOption[] = []; + private readonly allowedModelIds?: ReadonlySet; - constructor(model: string, effort?: string) { + constructor( + model: string, + effort?: string, + allowedModelIds?: ReadonlySet, + ) { this._model = model; this._effort = effort; + this.allowedModelIds = allowedModelIds?.size ? allowedModelIds : undefined; this.rebuild(); } @@ -262,8 +268,12 @@ export class SessionConfigState { ): { modeChanged: boolean } { let modeChanged = false; if (typeof value === "string") { - if (configId === "model") this._model = value; - else if (configId === "effort") this._effort = value; + if ( + configId === "model" && + (!this.allowedModelIds || this.allowedModelIds.has(value)) + ) { + this._model = value; + } else if (configId === "effort") this._effort = value; else if (configId === "mode") { this._mode = resolveCodexMode(value); modeChanged = true; @@ -279,13 +289,24 @@ export class SessionConfigState { * populate efforts, so fall back to the shared codex model→effort map. */ loadModels(rawModels: RawModel[]): void { - this.models = rawModels + const liveModels = rawModels .filter((m) => !m?.hidden) .filter((m) => isOpenAIModel(m as unknown as GatewayModel)) .map((m) => ({ id: (m.id ?? m.model) as string, name: (m.displayName ?? m.id ?? m.model) as string, })); + if (this.allowedModelIds) { + const liveModelsById = new Map( + liveModels.map((model) => [model.id, model]), + ); + this.models = [...this.allowedModelIds].map( + (modelId) => + liveModelsById.get(modelId) ?? { id: modelId, name: modelId }, + ); + } else { + this.models = liveModels; + } const current = rawModels.find( (m) => m.id === this._model || m.model === this._model, ); From 96c57fef4e37525852728b3f9d60852fd7169e8a Mon Sep 17 00:00:00 2001 From: Richard Solomou Date: Sun, 19 Jul 2026 14:03:34 +0300 Subject: [PATCH 2/3] fix(codex): preserve gateway model availability Authenticate the gateway catalog so restricted models remain gated, and keep the catalog available when native model discovery fails. Generated-By: PostHog Code Task-Id: dfa9be76-95bf-4166-8adc-529483e3d525 --- packages/agent/src/adapters/acp-connection.ts | 5 +- .../codex-app-server-agent.ts | 5 +- .../codex-app-server/session-config.test.ts | 57 ++++++++++++++-- .../codex-app-server/session-config.ts | 66 +++++++++++++++---- packages/agent/src/agent.test.ts | 9 +++ packages/agent/src/agent.ts | 33 +++++----- 6 files changed, 136 insertions(+), 39 deletions(-) diff --git a/packages/agent/src/adapters/acp-connection.ts b/packages/agent/src/adapters/acp-connection.ts index 61a0e97417..d04c1fef8e 100644 --- a/packages/agent/src/adapters/acp-connection.ts +++ b/packages/agent/src/adapters/acp-connection.ts @@ -1,5 +1,6 @@ import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"; import type { Adapter } from "@posthog/shared"; +import type { ModelInfo } from "../gateway-models"; import type { SessionLogWriter } from "../session-log-writer"; import type { PostHogAPIConfig, ProcessSpawnedCallback } from "../types"; import { Logger } from "../utils/logger"; @@ -24,7 +25,7 @@ export type AcpConnectionConfig = { logger?: Logger; processCallbacks?: ProcessSpawnedCallback; codexOptions?: CodexOptions; - allowedModelIds?: Set; + codexModels?: ReadonlyArray; /** Callback invoked when the agent calls the create_output tool for structured output */ onStructuredOutput?: (output: Record) => Promise; /** PostHog API config; when set, enables file-read enrichment unless disabled. */ @@ -230,7 +231,7 @@ function createCodexConnection(config: AcpConnectionConfig): AcpConnection { }, model: codexOptions.model, reasoningEffort: codexOptions.reasoningEffort, - allowedModelIds: config.allowedModelIds, + gatewayModels: config.codexModels, processCallbacks: config.processCallbacks, onStructuredOutput: config.onStructuredOutput, logger: config.logger?.child("CodexAppServerAgent"), diff --git a/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts b/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts index a18e379c4a..da6939687e 100644 --- a/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts +++ b/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts @@ -29,6 +29,7 @@ import { type NativeGoalState, POSTHOG_NOTIFICATIONS, } from "../../acp-extensions"; +import type { ModelInfo } from "../../gateway-models"; import { DEFAULT_CODEX_MODEL } from "../../gateway-models"; import type { ProcessSpawnedCallback } from "../../types"; import { ALLOW_BYPASS } from "../../utils/common"; @@ -203,7 +204,7 @@ export interface CodexAppServerAgentOptions { processOptions: CodexAppServerProcessOptions; model?: string; reasoningEffort?: string; - allowedModelIds?: ReadonlySet; + gatewayModels?: ReadonlyArray; processCallbacks?: ProcessSpawnedCallback; logger?: Logger; onStructuredOutput?: (output: Record) => Promise; @@ -266,7 +267,7 @@ export class CodexAppServerAgent extends BaseAcpAgent { this.config = new SessionConfigState( options.model ?? DEFAULT_CODEX_MODEL, options.reasoningEffort, - options.allowedModelIds, + options.gatewayModels, ); this.onStructuredOutput = options.onStructuredOutput; this.developerInstructions = options.processOptions.developerInstructions; diff --git a/packages/agent/src/adapters/codex-app-server/session-config.test.ts b/packages/agent/src/adapters/codex-app-server/session-config.test.ts index 876d2f90cf..643646ae34 100644 --- a/packages/agent/src/adapters/codex-app-server/session-config.test.ts +++ b/packages/agent/src/adapters/codex-app-server/session-config.test.ts @@ -1,3 +1,4 @@ +import { isRestrictedModelOption } from "@posthog/shared"; import { describe, expect, it } from "vitest"; import { buildCodexModes, @@ -136,22 +137,66 @@ describe("SessionConfigState", () => { }); it("uses gateway models when the app-server model list is stale", () => { - const config = new SessionConfigState( - "gpt-5.5", - undefined, - new Set(["gpt-5.5", "gpt-5.6-sol"]), - ); + const config = new SessionConfigState("gpt-5.5", undefined, [ + { id: "gpt-5.5", allowed: true }, + { id: "gpt-5.6-sol", allowed: true }, + { id: "gpt-5.6-terra", allowed: false }, + ]); config.loadModels([ { id: "gpt-5.5", model: "gpt-5.5", displayName: "GPT-5.5" }, + { + id: "gpt-5.6-terra", + model: "gpt-5.6-terra", + displayName: "GPT-5.6 Terra", + }, ]); const modelOption = config.options.find( (option) => option.category === "model", ); - expect(modelOption?.type === "select" ? modelOption.options : []).toEqual([ + const modelOptions = + modelOption?.type === "select" + ? (modelOption.options as Array<{ + name: string; + value: string; + _meta?: Record; + }>) + : []; + expect(modelOptions).toEqual([ { name: "GPT-5.5", value: "gpt-5.5" }, { name: "gpt-5.6-sol", value: "gpt-5.6-sol" }, + { + name: "GPT-5.6 Terra", + value: "gpt-5.6-terra", + _meta: { "posthog.code/restrictedModel": true }, + }, + ]); + + config.setOption("model", "gpt-5.6-terra"); + + expect(config.model).toBe("gpt-5.5"); + expect( + isRestrictedModelOption( + modelOptions.find((option) => option.value === "gpt-5.6-terra")?._meta, + ), + ).toBe(true); + }); + + it("keeps gateway models when the app-server model list fails", () => { + const config = new SessionConfigState("gpt-5.5", undefined, [ + { id: "gpt-5.5", allowed: true }, + { id: "gpt-5.6-sol", allowed: true }, + ]); + + config.clearModels(); + + const modelOption = config.options.find( + (option) => option.category === "model", + ); + expect(modelOption?.type === "select" ? modelOption.options : []).toEqual([ + { name: "gpt-5.5", value: "gpt-5.5" }, + { name: "gpt-5.6-sol", value: "gpt-5.6-sol" }, ]); }); }); diff --git a/packages/agent/src/adapters/codex-app-server/session-config.ts b/packages/agent/src/adapters/codex-app-server/session-config.ts index 9a0ef2e13c..244500950d 100644 --- a/packages/agent/src/adapters/codex-app-server/session-config.ts +++ b/packages/agent/src/adapters/codex-app-server/session-config.ts @@ -1,11 +1,19 @@ -import type { SessionConfigOption } from "@agentclientprotocol/sdk"; +import type { + SessionConfigOption, + SessionConfigSelectOption, +} from "@agentclientprotocol/sdk"; import { CODEX_MODE_PRESETS, type CodexModePreset, type ExecutionMode, resolveCloudInitialPermissionMode, + restrictedModelMeta, } from "@posthog/shared"; -import { type GatewayModel, isOpenAIModel } from "../../gateway-models"; +import { + type GatewayModel, + isOpenAIModel, + type ModelInfo, +} from "../../gateway-models"; import { getReasoningEffortOptions } from "./models"; /** @@ -158,7 +166,11 @@ export interface ConfigSelectors { model: string; effort?: string; /** From model/list; falls back to the single current model when empty. */ - models: Array<{ id: string; name: string }>; + models: Array<{ + id: string; + name: string; + _meta?: Record; + }>; efforts: string[]; } @@ -195,7 +207,13 @@ export function buildConfigOptions(s: ConfigSelectors): SessionConfigOption[] { name: "Model", category: "model", currentValue: s.model, - options: models.map((m) => ({ name: m.name, value: m.id })), + options: models.map( + (m): SessionConfigSelectOption => ({ + name: m.name, + value: m.id, + ...(m._meta ? { _meta: m._meta } : {}), + }), + ), } as unknown as SessionConfigOption, { type: "select", @@ -226,19 +244,31 @@ export class SessionConfigState { private _model: string; private _effort?: string; private _mode = DEFAULT_MODE; - private models: Array<{ id: string; name: string }> = []; + private models: Array<{ + id: string; + name: string; + _meta?: Record; + }> = []; private efforts: string[] = []; private _options: SessionConfigOption[] = []; + private readonly gatewayModels?: ReadonlyArray; private readonly allowedModelIds?: ReadonlySet; constructor( model: string, effort?: string, - allowedModelIds?: ReadonlySet, + gatewayModels?: ReadonlyArray, ) { this._model = model; this._effort = effort; - this.allowedModelIds = allowedModelIds?.size ? allowedModelIds : undefined; + this.gatewayModels = gatewayModels?.length ? gatewayModels : undefined; + this.allowedModelIds = this.gatewayModels + ? new Set( + this.gatewayModels + .filter((gatewayModel) => gatewayModel.allowed) + .map((gatewayModel) => gatewayModel.id), + ) + : undefined; this.rebuild(); } @@ -270,7 +300,7 @@ export class SessionConfigState { if (typeof value === "string") { if ( configId === "model" && - (!this.allowedModelIds || this.allowedModelIds.has(value)) + (!this.gatewayModels || this.allowedModelIds?.has(value)) ) { this._model = value; } else if (configId === "effort") this._effort = value; @@ -296,14 +326,17 @@ export class SessionConfigState { id: (m.id ?? m.model) as string, name: (m.displayName ?? m.id ?? m.model) as string, })); - if (this.allowedModelIds) { + if (this.gatewayModels) { const liveModelsById = new Map( liveModels.map((model) => [model.id, model]), ); - this.models = [...this.allowedModelIds].map( - (modelId) => - liveModelsById.get(modelId) ?? { id: modelId, name: modelId }, - ); + this.models = this.gatewayModels.map((gatewayModel) => ({ + ...(liveModelsById.get(gatewayModel.id) ?? { + id: gatewayModel.id, + name: gatewayModel.id, + }), + ...(gatewayModel.allowed ? {} : { _meta: restrictedModelMeta() }), + })); } else { this.models = liveModels; } @@ -321,7 +354,12 @@ export class SessionConfigState { /** Reset the model/effort lists (model/list failed); keeps the current model. */ clearModels(): void { - this.models = []; + this.models = + this.gatewayModels?.map((gatewayModel) => ({ + id: gatewayModel.id, + name: gatewayModel.id, + ...(gatewayModel.allowed ? {} : { _meta: restrictedModelMeta() }), + })) ?? []; this.efforts = []; this.rebuild(); } diff --git a/packages/agent/src/agent.test.ts b/packages/agent/src/agent.test.ts index 58ad848186..79f815509e 100644 --- a/packages/agent/src/agent.test.ts +++ b/packages/agent/src/agent.test.ts @@ -54,5 +54,14 @@ describe("Agent", () => { reasoningEffort: "xhigh", }), ); + expect(config.codexModels).toEqual([ + expect.objectContaining({ id: "gpt-5.5", allowed: true }), + ]); + expect(fetchMock).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + headers: { Authorization: "Bearer token" }, + }), + ); }); }); diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index 69ae25c34c..63b3830849 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -9,6 +9,7 @@ import { DEFAULT_GATEWAY_MODEL, fetchModelsList, isBlockedModelId, + type ModelInfo, } from "./gateway-models"; import { PostHogAPIClient, type TaskRunUpdate } from "./posthog-api"; import { SessionLogWriter } from "./session-log-writer"; @@ -78,7 +79,7 @@ export class Agent { const gatewayConfig = await this._resolveGatewayConfig(options.gatewayUrl); this.taskRunId = taskRunId; - let allowedModelIds: Set | undefined; + let codexModels: ModelInfo[] | undefined; let sanitizedModel = options.model && !isBlockedModelId(options.model) ? options.model @@ -86,25 +87,27 @@ export class Agent { if (options.adapter === "codex" && gatewayConfig) { const models = await fetchModelsList({ gatewayUrl: gatewayConfig.gatewayUrl, + authToken: gatewayConfig.apiKey, }); - const codexModelIds = models - .filter((model) => { - if (isBlockedModelId(model.id)) return false; - if (model.owned_by) { - return model.owned_by === "openai"; - } - return model.id.startsWith("gpt-") || model.id.startsWith("openai/"); - }) + const gatewayCodexModels = models.filter((model) => { + if (isBlockedModelId(model.id)) return false; + if (model.owned_by) { + return model.owned_by === "openai"; + } + return model.id.startsWith("gpt-") || model.id.startsWith("openai/"); + }); + const allowedModelIds = gatewayCodexModels + .filter((model) => model.allowed) .map((model) => model.id); - if (codexModelIds.length > 0) { - allowedModelIds = new Set(codexModelIds); + if (gatewayCodexModels.length > 0) { + codexModels = gatewayCodexModels; } - if (!sanitizedModel || !allowedModelIds?.has(sanitizedModel)) { - sanitizedModel = codexModelIds.includes(DEFAULT_CODEX_MODEL) + if (!sanitizedModel || !allowedModelIds.includes(sanitizedModel)) { + sanitizedModel = allowedModelIds.includes(DEFAULT_CODEX_MODEL) ? DEFAULT_CODEX_MODEL - : codexModelIds[0]; + : (allowedModelIds[0] ?? sanitizedModel); } } if (!sanitizedModel && options.adapter !== "codex") { @@ -130,7 +133,7 @@ export class Agent { logger: this.logger, processCallbacks: options.processCallbacks, onStructuredOutput: options.onStructuredOutput, - allowedModelIds, + codexModels, posthogApiConfig: this.posthogApiConfig, enricherEnabled: this.enricherEnabled, claudeGatewayEnv, From b20bf122561f41197c6ff8b1c64931649b674ed2 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 04:16:14 +0000 Subject: [PATCH 3/3] chore(visual): update storybook baselines 1 updated Run: 03b80f6e-116e-4b88-98f8-7303071de456 Co-authored-by: richardsolomou <2622273+richardsolomou@users.noreply.github.com> --- apps/code/snapshots.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/code/snapshots.yml b/apps/code/snapshots.yml index cfbdd290b1..26cb8c4587 100644 --- a/apps/code/snapshots.yml +++ b/apps/code/snapshots.yml @@ -27,7 +27,7 @@ snapshots: archive-archivedtasksview--many-tasks--dark: hash: v1.k4693efd2.40d24f07a24c6400c32ae68b39fd908a056beb5dc6df8bcb237ec2ab2b494f4a.l2vjfSQDalCPRtDNV7pbfTKGlMsQoI81jI-UdJfLHWE archive-archivedtasksview--many-tasks--light: - hash: v1.k4693efd2.52ec9963bfe6f248ffcdaa4c26945d2b7e303115825b65d978aa5aefd4af1007.ZLtOfTZUrznhYeB-N2SIHzQWRzF6TDSaipNkfd8uzjM + hash: v1.k4693efd2.74c25b303262f2d4c0f22890d351e76f482827548548f7c023bc309327c927b8.DP9VVrvBz1naIJMwHORkfqm69BlO785ulOt4o6zFs94 archive-archivedtasksview--mixed-modes--dark: hash: v1.k4693efd2.d94039b8cc17a4ad1b720364f41fee58a4843aa9a901443997ba62602f698794.441LWZhT-2WQZJHni4LoOgQsOSr2O103NZOk1pF8ME4 archive-archivedtasksview--mixed-modes--light: