Skip to content

Commit f41d71e

Browse files
fix(codex): restore gateway models after reconnect (#3566)
Co-authored-by: posthog[bot] <206114724+posthog[bot]@users.noreply.github.com> Co-authored-by: richardsolomou <2622273+richardsolomou@users.noreply.github.com>
1 parent f418475 commit f41d71e

7 files changed

Lines changed: 168 additions & 27 deletions

File tree

apps/code/snapshots.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ snapshots:
2727
archive-archivedtasksview--many-tasks--dark:
2828
hash: v1.k4693efd2.40d24f07a24c6400c32ae68b39fd908a056beb5dc6df8bcb237ec2ab2b494f4a.l2vjfSQDalCPRtDNV7pbfTKGlMsQoI81jI-UdJfLHWE
2929
archive-archivedtasksview--many-tasks--light:
30-
hash: v1.k4693efd2.52ec9963bfe6f248ffcdaa4c26945d2b7e303115825b65d978aa5aefd4af1007.ZLtOfTZUrznhYeB-N2SIHzQWRzF6TDSaipNkfd8uzjM
30+
hash: v1.k4693efd2.74c25b303262f2d4c0f22890d351e76f482827548548f7c023bc309327c927b8.DP9VVrvBz1naIJMwHORkfqm69BlO785ulOt4o6zFs94
3131
archive-archivedtasksview--mixed-modes--dark:
3232
hash: v1.k4693efd2.d94039b8cc17a4ad1b720364f41fee58a4843aa9a901443997ba62602f698794.441LWZhT-2WQZJHni4LoOgQsOSr2O103NZOk1pF8ME4
3333
archive-archivedtasksview--mixed-modes--light:

packages/agent/src/adapters/acp-connection.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
22
import type { Adapter } from "@posthog/shared";
3+
import type { ModelInfo } from "../gateway-models";
34
import type { SessionLogWriter } from "../session-log-writer";
45
import type { PostHogAPIConfig, ProcessSpawnedCallback } from "../types";
56
import { Logger } from "../utils/logger";
@@ -24,7 +25,7 @@ export type AcpConnectionConfig = {
2425
logger?: Logger;
2526
processCallbacks?: ProcessSpawnedCallback;
2627
codexOptions?: CodexOptions;
27-
allowedModelIds?: Set<string>;
28+
codexModels?: ReadonlyArray<ModelInfo>;
2829
/** Callback invoked when the agent calls the create_output tool for structured output */
2930
onStructuredOutput?: (output: Record<string, unknown>) => Promise<void>;
3031
/** PostHog API config; when set, enables file-read enrichment unless disabled. */
@@ -230,6 +231,7 @@ function createCodexConnection(config: AcpConnectionConfig): AcpConnection {
230231
},
231232
model: codexOptions.model,
232233
reasoningEffort: codexOptions.reasoningEffort,
234+
gatewayModels: config.codexModels,
233235
processCallbacks: config.processCallbacks,
234236
onStructuredOutput: config.onStructuredOutput,
235237
logger: config.logger?.child("CodexAppServerAgent"),

packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
type NativeGoalState,
3030
POSTHOG_NOTIFICATIONS,
3131
} from "../../acp-extensions";
32+
import type { ModelInfo } from "../../gateway-models";
3233
import { DEFAULT_CODEX_MODEL } from "../../gateway-models";
3334
import type { ProcessSpawnedCallback } from "../../types";
3435
import { ALLOW_BYPASS } from "../../utils/common";
@@ -203,6 +204,7 @@ export interface CodexAppServerAgentOptions {
203204
processOptions: CodexAppServerProcessOptions;
204205
model?: string;
205206
reasoningEffort?: string;
207+
gatewayModels?: ReadonlyArray<ModelInfo>;
206208
processCallbacks?: ProcessSpawnedCallback;
207209
logger?: Logger;
208210
onStructuredOutput?: (output: Record<string, unknown>) => Promise<void>;
@@ -265,6 +267,7 @@ export class CodexAppServerAgent extends BaseAcpAgent {
265267
this.config = new SessionConfigState(
266268
options.model ?? DEFAULT_CODEX_MODEL,
267269
options.reasoningEffort,
270+
options.gatewayModels,
268271
);
269272
this.onStructuredOutput = options.onStructuredOutput;
270273
this.developerInstructions = options.processOptions.developerInstructions;

packages/agent/src/adapters/codex-app-server/session-config.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isRestrictedModelOption } from "@posthog/shared";
12
import { describe, expect, it } from "vitest";
23
import {
34
buildCodexModes,
@@ -134,6 +135,70 @@ describe("SessionConfigState", () => {
134135
config.options.find((option) => option.category === "mode")?.currentValue,
135136
).toBe("full-access");
136137
});
138+
139+
it("uses gateway models when the app-server model list is stale", () => {
140+
const config = new SessionConfigState("gpt-5.5", undefined, [
141+
{ id: "gpt-5.5", allowed: true },
142+
{ id: "gpt-5.6-sol", allowed: true },
143+
{ id: "gpt-5.6-terra", allowed: false },
144+
]);
145+
146+
config.loadModels([
147+
{ id: "gpt-5.5", model: "gpt-5.5", displayName: "GPT-5.5" },
148+
{
149+
id: "gpt-5.6-terra",
150+
model: "gpt-5.6-terra",
151+
displayName: "GPT-5.6 Terra",
152+
},
153+
]);
154+
155+
const modelOption = config.options.find(
156+
(option) => option.category === "model",
157+
);
158+
const modelOptions =
159+
modelOption?.type === "select"
160+
? (modelOption.options as Array<{
161+
name: string;
162+
value: string;
163+
_meta?: Record<string, unknown>;
164+
}>)
165+
: [];
166+
expect(modelOptions).toEqual([
167+
{ name: "GPT-5.5", value: "gpt-5.5" },
168+
{ name: "gpt-5.6-sol", value: "gpt-5.6-sol" },
169+
{
170+
name: "GPT-5.6 Terra",
171+
value: "gpt-5.6-terra",
172+
_meta: { "posthog.code/restrictedModel": true },
173+
},
174+
]);
175+
176+
config.setOption("model", "gpt-5.6-terra");
177+
178+
expect(config.model).toBe("gpt-5.5");
179+
expect(
180+
isRestrictedModelOption(
181+
modelOptions.find((option) => option.value === "gpt-5.6-terra")?._meta,
182+
),
183+
).toBe(true);
184+
});
185+
186+
it("keeps gateway models when the app-server model list fails", () => {
187+
const config = new SessionConfigState("gpt-5.5", undefined, [
188+
{ id: "gpt-5.5", allowed: true },
189+
{ id: "gpt-5.6-sol", allowed: true },
190+
]);
191+
192+
config.clearModels();
193+
194+
const modelOption = config.options.find(
195+
(option) => option.category === "model",
196+
);
197+
expect(modelOption?.type === "select" ? modelOption.options : []).toEqual([
198+
{ name: "gpt-5.5", value: "gpt-5.5" },
199+
{ name: "gpt-5.6-sol", value: "gpt-5.6-sol" },
200+
]);
201+
});
137202
});
138203

139204
describe("buildConfigOptions", () => {

packages/agent/src/adapters/codex-app-server/session-config.ts

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
import type { SessionConfigOption } from "@agentclientprotocol/sdk";
1+
import type {
2+
SessionConfigOption,
3+
SessionConfigSelectOption,
4+
} from "@agentclientprotocol/sdk";
25
import {
36
CODEX_MODE_PRESETS,
47
type CodexModePreset,
58
type ExecutionMode,
69
resolveCloudInitialPermissionMode,
10+
restrictedModelMeta,
711
} from "@posthog/shared";
8-
import { type GatewayModel, isOpenAIModel } from "../../gateway-models";
12+
import {
13+
type GatewayModel,
14+
isOpenAIModel,
15+
type ModelInfo,
16+
} from "../../gateway-models";
917
import { getReasoningEffortOptions } from "./models";
1018

1119
/**
@@ -158,7 +166,11 @@ export interface ConfigSelectors {
158166
model: string;
159167
effort?: string;
160168
/** From model/list; falls back to the single current model when empty. */
161-
models: Array<{ id: string; name: string }>;
169+
models: Array<{
170+
id: string;
171+
name: string;
172+
_meta?: Record<string, unknown>;
173+
}>;
162174
efforts: string[];
163175
}
164176

@@ -195,7 +207,13 @@ export function buildConfigOptions(s: ConfigSelectors): SessionConfigOption[] {
195207
name: "Model",
196208
category: "model",
197209
currentValue: s.model,
198-
options: models.map((m) => ({ name: m.name, value: m.id })),
210+
options: models.map(
211+
(m): SessionConfigSelectOption => ({
212+
name: m.name,
213+
value: m.id,
214+
...(m._meta ? { _meta: m._meta } : {}),
215+
}),
216+
),
199217
} as unknown as SessionConfigOption,
200218
{
201219
type: "select",
@@ -226,13 +244,31 @@ export class SessionConfigState {
226244
private _model: string;
227245
private _effort?: string;
228246
private _mode = DEFAULT_MODE;
229-
private models: Array<{ id: string; name: string }> = [];
247+
private models: Array<{
248+
id: string;
249+
name: string;
250+
_meta?: Record<string, unknown>;
251+
}> = [];
230252
private efforts: string[] = [];
231253
private _options: SessionConfigOption[] = [];
254+
private readonly gatewayModels?: ReadonlyArray<ModelInfo>;
255+
private readonly allowedModelIds?: ReadonlySet<string>;
232256

233-
constructor(model: string, effort?: string) {
257+
constructor(
258+
model: string,
259+
effort?: string,
260+
gatewayModels?: ReadonlyArray<ModelInfo>,
261+
) {
234262
this._model = model;
235263
this._effort = effort;
264+
this.gatewayModels = gatewayModels?.length ? gatewayModels : undefined;
265+
this.allowedModelIds = this.gatewayModels
266+
? new Set(
267+
this.gatewayModels
268+
.filter((gatewayModel) => gatewayModel.allowed)
269+
.map((gatewayModel) => gatewayModel.id),
270+
)
271+
: undefined;
236272
this.rebuild();
237273
}
238274

@@ -262,8 +298,12 @@ export class SessionConfigState {
262298
): { modeChanged: boolean } {
263299
let modeChanged = false;
264300
if (typeof value === "string") {
265-
if (configId === "model") this._model = value;
266-
else if (configId === "effort") this._effort = value;
301+
if (
302+
configId === "model" &&
303+
(!this.gatewayModels || this.allowedModelIds?.has(value))
304+
) {
305+
this._model = value;
306+
} else if (configId === "effort") this._effort = value;
267307
else if (configId === "mode") {
268308
this._mode = resolveCodexMode(value);
269309
modeChanged = true;
@@ -279,13 +319,27 @@ export class SessionConfigState {
279319
* populate efforts, so fall back to the shared codex model→effort map.
280320
*/
281321
loadModels(rawModels: RawModel[]): void {
282-
this.models = rawModels
322+
const liveModels = rawModels
283323
.filter((m) => !m?.hidden)
284324
.filter((m) => isOpenAIModel(m as unknown as GatewayModel))
285325
.map((m) => ({
286326
id: (m.id ?? m.model) as string,
287327
name: (m.displayName ?? m.id ?? m.model) as string,
288328
}));
329+
if (this.gatewayModels) {
330+
const liveModelsById = new Map(
331+
liveModels.map((model) => [model.id, model]),
332+
);
333+
this.models = this.gatewayModels.map((gatewayModel) => ({
334+
...(liveModelsById.get(gatewayModel.id) ?? {
335+
id: gatewayModel.id,
336+
name: gatewayModel.id,
337+
}),
338+
...(gatewayModel.allowed ? {} : { _meta: restrictedModelMeta() }),
339+
}));
340+
} else {
341+
this.models = liveModels;
342+
}
289343
const current = rawModels.find(
290344
(m) => m.id === this._model || m.model === this._model,
291345
);
@@ -300,7 +354,12 @@ export class SessionConfigState {
300354

301355
/** Reset the model/effort lists (model/list failed); keeps the current model. */
302356
clearModels(): void {
303-
this.models = [];
357+
this.models =
358+
this.gatewayModels?.map((gatewayModel) => ({
359+
id: gatewayModel.id,
360+
name: gatewayModel.id,
361+
...(gatewayModel.allowed ? {} : { _meta: restrictedModelMeta() }),
362+
})) ?? [];
304363
this.efforts = [];
305364
this.rebuild();
306365
}

packages/agent/src/agent.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,14 @@ describe("Agent", () => {
5454
reasoningEffort: "xhigh",
5555
}),
5656
);
57+
expect(config.codexModels).toEqual([
58+
expect.objectContaining({ id: "gpt-5.5", allowed: true }),
59+
]);
60+
expect(fetchMock).toHaveBeenCalledWith(
61+
expect.any(String),
62+
expect.objectContaining({
63+
headers: { Authorization: "Bearer token" },
64+
}),
65+
);
5766
});
5867
});

packages/agent/src/agent.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
DEFAULT_GATEWAY_MODEL,
1010
fetchModelsList,
1111
isBlockedModelId,
12+
type ModelInfo,
1213
} from "./gateway-models";
1314
import { PostHogAPIClient, type TaskRunUpdate } from "./posthog-api";
1415
import { SessionLogWriter } from "./session-log-writer";
@@ -78,33 +79,35 @@ export class Agent {
7879
const gatewayConfig = await this._resolveGatewayConfig(options.gatewayUrl);
7980
this.taskRunId = taskRunId;
8081

81-
let allowedModelIds: Set<string> | undefined;
82+
let codexModels: ModelInfo[] | undefined;
8283
let sanitizedModel =
8384
options.model && !isBlockedModelId(options.model)
8485
? options.model
8586
: undefined;
8687
if (options.adapter === "codex" && gatewayConfig) {
8788
const models = await fetchModelsList({
8889
gatewayUrl: gatewayConfig.gatewayUrl,
90+
authToken: gatewayConfig.apiKey,
8991
});
90-
const codexModelIds = models
91-
.filter((model) => {
92-
if (isBlockedModelId(model.id)) return false;
93-
if (model.owned_by) {
94-
return model.owned_by === "openai";
95-
}
96-
return model.id.startsWith("gpt-") || model.id.startsWith("openai/");
97-
})
92+
const gatewayCodexModels = models.filter((model) => {
93+
if (isBlockedModelId(model.id)) return false;
94+
if (model.owned_by) {
95+
return model.owned_by === "openai";
96+
}
97+
return model.id.startsWith("gpt-") || model.id.startsWith("openai/");
98+
});
99+
const allowedModelIds = gatewayCodexModels
100+
.filter((model) => model.allowed)
98101
.map((model) => model.id);
99102

100-
if (codexModelIds.length > 0) {
101-
allowedModelIds = new Set(codexModelIds);
103+
if (gatewayCodexModels.length > 0) {
104+
codexModels = gatewayCodexModels;
102105
}
103106

104-
if (!sanitizedModel || !allowedModelIds?.has(sanitizedModel)) {
105-
sanitizedModel = codexModelIds.includes(DEFAULT_CODEX_MODEL)
107+
if (!sanitizedModel || !allowedModelIds.includes(sanitizedModel)) {
108+
sanitizedModel = allowedModelIds.includes(DEFAULT_CODEX_MODEL)
106109
? DEFAULT_CODEX_MODEL
107-
: codexModelIds[0];
110+
: (allowedModelIds[0] ?? sanitizedModel);
108111
}
109112
}
110113
if (!sanitizedModel && options.adapter !== "codex") {
@@ -130,7 +133,7 @@ export class Agent {
130133
logger: this.logger,
131134
processCallbacks: options.processCallbacks,
132135
onStructuredOutput: options.onStructuredOutput,
133-
allowedModelIds,
136+
codexModels,
134137
posthogApiConfig: this.posthogApiConfig,
135138
enricherEnabled: this.enricherEnabled,
136139
claudeGatewayEnv,

0 commit comments

Comments
 (0)