Skip to content

Commit 96c57fe

Browse files
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
1 parent 81070b2 commit 96c57fe

6 files changed

Lines changed: 136 additions & 39 deletions

File tree

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

Lines changed: 3 additions & 2 deletions
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,7 +231,7 @@ function createCodexConnection(config: AcpConnectionConfig): AcpConnection {
230231
},
231232
model: codexOptions.model,
232233
reasoningEffort: codexOptions.reasoningEffort,
233-
allowedModelIds: config.allowedModelIds,
234+
gatewayModels: config.codexModels,
234235
processCallbacks: config.processCallbacks,
235236
onStructuredOutput: config.onStructuredOutput,
236237
logger: config.logger?.child("CodexAppServerAgent"),

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

Lines changed: 3 additions & 2 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,7 +204,7 @@ export interface CodexAppServerAgentOptions {
203204
processOptions: CodexAppServerProcessOptions;
204205
model?: string;
205206
reasoningEffort?: string;
206-
allowedModelIds?: ReadonlySet<string>;
207+
gatewayModels?: ReadonlyArray<ModelInfo>;
207208
processCallbacks?: ProcessSpawnedCallback;
208209
logger?: Logger;
209210
onStructuredOutput?: (output: Record<string, unknown>) => Promise<void>;
@@ -266,7 +267,7 @@ export class CodexAppServerAgent extends BaseAcpAgent {
266267
this.config = new SessionConfigState(
267268
options.model ?? DEFAULT_CODEX_MODEL,
268269
options.reasoningEffort,
269-
options.allowedModelIds,
270+
options.gatewayModels,
270271
);
271272
this.onStructuredOutput = options.onStructuredOutput;
272273
this.developerInstructions = options.processOptions.developerInstructions;

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

Lines changed: 51 additions & 6 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,
@@ -136,22 +137,66 @@ describe("SessionConfigState", () => {
136137
});
137138

138139
it("uses gateway models when the app-server model list is stale", () => {
139-
const config = new SessionConfigState(
140-
"gpt-5.5",
141-
undefined,
142-
new Set(["gpt-5.5", "gpt-5.6-sol"]),
143-
);
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+
]);
144145

145146
config.loadModels([
146147
{ 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+
},
147153
]);
148154

149155
const modelOption = config.options.find(
150156
(option) => option.category === "model",
151157
);
152-
expect(modelOption?.type === "select" ? modelOption.options : []).toEqual([
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([
153167
{ name: "GPT-5.5", value: "gpt-5.5" },
154168
{ 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" },
155200
]);
156201
});
157202
});

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

Lines changed: 52 additions & 14 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,19 +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>;
232255
private readonly allowedModelIds?: ReadonlySet<string>;
233256

234257
constructor(
235258
model: string,
236259
effort?: string,
237-
allowedModelIds?: ReadonlySet<string>,
260+
gatewayModels?: ReadonlyArray<ModelInfo>,
238261
) {
239262
this._model = model;
240263
this._effort = effort;
241-
this.allowedModelIds = allowedModelIds?.size ? allowedModelIds : undefined;
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;
242272
this.rebuild();
243273
}
244274

@@ -270,7 +300,7 @@ export class SessionConfigState {
270300
if (typeof value === "string") {
271301
if (
272302
configId === "model" &&
273-
(!this.allowedModelIds || this.allowedModelIds.has(value))
303+
(!this.gatewayModels || this.allowedModelIds?.has(value))
274304
) {
275305
this._model = value;
276306
} else if (configId === "effort") this._effort = value;
@@ -296,14 +326,17 @@ export class SessionConfigState {
296326
id: (m.id ?? m.model) as string,
297327
name: (m.displayName ?? m.id ?? m.model) as string,
298328
}));
299-
if (this.allowedModelIds) {
329+
if (this.gatewayModels) {
300330
const liveModelsById = new Map(
301331
liveModels.map((model) => [model.id, model]),
302332
);
303-
this.models = [...this.allowedModelIds].map(
304-
(modelId) =>
305-
liveModelsById.get(modelId) ?? { id: modelId, name: modelId },
306-
);
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+
}));
307340
} else {
308341
this.models = liveModels;
309342
}
@@ -321,7 +354,12 @@ export class SessionConfigState {
321354

322355
/** Reset the model/effort lists (model/list failed); keeps the current model. */
323356
clearModels(): void {
324-
this.models = [];
357+
this.models =
358+
this.gatewayModels?.map((gatewayModel) => ({
359+
id: gatewayModel.id,
360+
name: gatewayModel.id,
361+
...(gatewayModel.allowed ? {} : { _meta: restrictedModelMeta() }),
362+
})) ?? [];
325363
this.efforts = [];
326364
this.rebuild();
327365
}

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)