Skip to content

Commit 07af94d

Browse files
Mezzleclaude
andcommitted
fix(codex): default cyber access to Standard, matching Codex
Omitting cyberAccessProgram leaves the choice to OpenAI's server, while Codex's own clients send Standard unless Daybreak is on. Models in the catalog now default to Standard, or to Daybreak when Codex's daybreak setting is on, and T3 sends the program on every turn. Turns with no selection use the model's default from the provider snapshot. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 5492df8 commit 07af94d

8 files changed

Lines changed: 200 additions & 60 deletions

File tree

‎apps/server/src/codexModelOptions.ts‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,8 @@ export function getCodexServiceTierOptionValue(
1313
);
1414
}
1515

16-
export const CODEX_AUTOMATIC_CYBER_ACCESS_PROGRAM_ID = "automatic";
17-
18-
/** Undefined leaves Codex's automatic cyber treatment in place. */
1916
export function getCodexCyberAccessProgramOptionValue(
2017
modelSelection: ModelSelection | null | undefined,
2118
): string | undefined {
22-
const value = getModelSelectionStringOptionValue(modelSelection, "cyberAccessProgram");
23-
return value === CODEX_AUTOMATIC_CYBER_ACCESS_PROGRAM_ID ? undefined : value;
19+
return getModelSelectionStringOptionValue(modelSelection, "cyberAccessProgram");
2420
}

‎apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,9 @@ describe("CodexAdapterV2 runtime policy", () => {
221221
}),
222222
);
223223

224-
it.effect("leaves Codex's automatic cyber access in place unless a program is chosen", () =>
224+
it.effect("sends the chosen cyber access program, else the model's default", () =>
225225
Effect.gen(function* () {
226-
const build = (value?: string) =>
226+
const build = (value?: string, defaultCyberAccessProgram?: string) =>
227227
CodexAdapterV2.buildCodexTurnStartParams({
228228
nativeThreadId: "native-cyber",
229229
codexInput: [{ type: "text", text: "test" }],
@@ -233,12 +233,15 @@ describe("CodexAdapterV2 runtime policy", () => {
233233
model: "gpt-5.4",
234234
...(value === undefined ? {} : { options: [{ id: "cyberAccessProgram", value }] }),
235235
},
236+
defaultCyberAccessProgram,
236237
});
237238

239+
// Custom models have no catalog entry, so Codex keeps its own treatment.
238240
assert.notProperty(yield* build(), "cyberAccessProgram");
239-
assert.notProperty(yield* build("automatic"), "cyberAccessProgram");
240-
assert.equal((yield* build("standard")).cyberAccessProgram, "standard");
241-
assert.equal((yield* build("daybreakBlue")).cyberAccessProgram, "daybreakBlue");
241+
assert.equal((yield* build(undefined, "standard")).cyberAccessProgram, "standard");
242+
assert.equal((yield* build("automatic", "standard")).cyberAccessProgram, "standard");
243+
assert.equal((yield* build("daybreakBlue", "standard")).cyberAccessProgram, "daybreakBlue");
244+
assert.equal((yield* build("standard", "daybreakBlue")).cyberAccessProgram, "standard");
242245
}),
243246
);
244247

‎apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,8 @@ function codexRuntimeModeTurnDefaults(runtimeMode: RuntimeMode): {
727727
}
728728
}
729729

730+
const isCodexCyberAccessProgram = Schema.is(CodexSchema.V2TurnStartParams__CyberAccessProgram);
731+
730732
export const buildCodexTurnStartParams = Effect.fn("CodexAdapterV2.buildCodexTurnStartParams")(
731733
function* (input: {
732734
readonly nativeThreadId: string;
@@ -739,6 +741,7 @@ export const buildCodexTurnStartParams = Effect.fn("CodexAdapterV2.buildCodexTur
739741
/** ChatGPT token sharing does not accept service tiers. */
740742
readonly omitServiceTier?: boolean;
741743
readonly appContext?: ProviderAdapter.ProviderAdapterV2TurnInput["appContext"];
744+
readonly defaultCyberAccessProgram?: string | undefined;
742745
}) {
743746
const runtimeModeDefaults = codexRuntimeModeTurnDefaults(input.runtimePolicy.runtimeMode);
744747
const approvalPolicy =
@@ -758,7 +761,10 @@ export const buildCodexTurnStartParams = Effect.fn("CodexAdapterV2.buildCodexTur
758761
input.omitServiceTier === true
759762
? undefined
760763
: getCodexServiceTierOptionValue(input.modelSelection);
761-
const cyberAccessProgram = getCodexCyberAccessProgramOptionValue(input.modelSelection);
764+
const selectedCyberAccessProgram = getCodexCyberAccessProgramOptionValue(input.modelSelection);
765+
const cyberAccessProgram = [selectedCyberAccessProgram, input.defaultCyberAccessProgram].find(
766+
isCodexCyberAccessProgram,
767+
);
762768
// MCP-written app context must be quoted user input, not developer instructions.
763769
const appContext = Object.fromEntries(
764770
(input.appContext ?? []).map((entry) => [
@@ -1557,7 +1563,10 @@ export type CodexAdapterV2DriverEnv =
15571563

15581564
export const createCodexAdapterV2 = Effect.fn("CodexAdapterV2.createCodexAdapterV2")(function* (
15591565
{ instanceId, environment, enabled, config }: ProviderAdapterDriverCreateInput<CodexSettings>,
1560-
hooks: Pick<CodexAdapterV2Options, "onUsageLimits" | "resolveRuntime" | "compatibility"> = {},
1566+
hooks: Pick<
1567+
CodexAdapterV2Options,
1568+
"onUsageLimits" | "resolveRuntime" | "compatibility" | "defaultCyberAccessProgram"
1569+
> = {},
15611570
) {
15621571
const clientFactory = yield* CodexAppServerClientFactory;
15631572
const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
@@ -1607,6 +1616,7 @@ export interface CodexAdapterV2Options {
16071616
readonly environment: NodeJS.ProcessEnv;
16081617
readonly clientFactory: CodexAppServerClientFactoryShape;
16091618
readonly onUsageLimits?: ManagedServerProvider["applyUsageLimits"];
1619+
readonly defaultCyberAccessProgram?: (model: string) => Effect.Effect<string | undefined>;
16101620
readonly resolveRuntime?: Effect.Effect<CodexEffectiveRuntime, ProviderSetupError, Scope.Scope>;
16111621
readonly compatibility?: (
16121622
version: string,
@@ -6636,7 +6646,12 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
66366646
) {
66376647
const threadId = yield* getNativeThreadId(turnInput.providerThread);
66386648
const mcpSession = yield* mcpSessions.read(turnInput.threadId);
6649+
const defaultCyberAccessProgram =
6650+
adapterOptions.defaultCyberAccessProgram === undefined
6651+
? undefined
6652+
: yield* adapterOptions.defaultCyberAccessProgram(turnInput.modelSelection.model);
66396653
const turnStartParams = yield* buildCodexTurnStartParams({
6654+
defaultCyberAccessProgram,
66406655
nativeThreadId: threadId,
66416656
codexInput,
66426657
runtimePolicy: turnInput.runtimePolicy,

‎apps/server/src/provider/CodexProvider.test.ts‎

Lines changed: 80 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { assert, it } from "@effect/vitest";
33
import {
44
appendCustomCodexModels,
55
applyPreferredCodexDefaultModel,
6+
applyConfiguredCodexCyberAccess,
7+
codexDefaultCyberAccessProgram,
68
mapCodexModelCapabilities,
79
} from "./CodexProvider.ts";
810

@@ -158,7 +160,7 @@ it("ignores custom models that shadow a preferred slug", () => {
158160
assert.deepStrictEqual(models.find((model) => model.isDefault)?.slug, "gpt-5.4");
159161
});
160162

161-
it("offers cyber access programs only when the catalog has a Daybreak program", () => {
163+
it("defaults cyber access to Standard when the catalog offers it", () => {
162164
const model = {
163165
additionalSpeedTiers: [],
164166
defaultReasoningEffort: "medium",
@@ -170,33 +172,86 @@ it("offers cyber access programs only when the catalog has a Daybreak program",
170172
model: "gpt-test",
171173
supportedReasoningEfforts: [],
172174
};
173-
174-
assert.deepStrictEqual(
175-
mapCodexModelCapabilities({
176-
...model,
177-
availableAccessPrograms: { cyber: ["standard"] },
178-
}).optionDescriptors,
179-
[],
180-
);
181-
assert.deepStrictEqual(
175+
const cyber = (programs?: ReadonlyArray<"standard" | "daybreakBlue">) =>
182176
mapCodexModelCapabilities({
183177
...model,
184-
availableAccessPrograms: { cyber: ["standard", "daybreakBlue"] },
185-
}).optionDescriptors,
186-
[
187-
{
188-
id: "cyberAccessProgram",
189-
label: "Cyber Access",
190-
type: "select",
191-
options: [
192-
{ id: "automatic", label: "Automatic", isDefault: true },
193-
{ id: "standard", label: "Standard" },
194-
{ id: "daybreakBlue", label: "Daybreak Blue" },
195-
],
196-
currentValue: "automatic",
197-
},
178+
...(programs ? { availableAccessPrograms: { cyber: [...programs] } } : {}),
179+
}).optionDescriptors?.[0];
180+
181+
assert.strictEqual(cyber(), undefined);
182+
assert.deepStrictEqual(cyber(["standard", "daybreakBlue"]), {
183+
id: "cyberAccessProgram",
184+
label: "Cyber Access",
185+
type: "select",
186+
options: [
187+
{ id: "standard", label: "Standard", isDefault: true },
188+
{ id: "daybreakBlue", label: "Daybreak Blue" },
198189
],
199-
);
190+
currentValue: "standard",
191+
});
192+
assert.deepInclude(cyber(["standard"]), { currentValue: "standard" });
193+
assert.deepInclude(cyber(["daybreakBlue"]), { currentValue: "daybreakBlue" });
194+
});
195+
196+
it("finds the default cyber access program for a model", () => {
197+
const capabilities = mapCodexModelCapabilities({
198+
additionalSpeedTiers: [],
199+
availableAccessPrograms: { cyber: ["standard", "daybreakBlue"] },
200+
defaultReasoningEffort: "medium",
201+
description: "Test model",
202+
displayName: "GPT Test",
203+
hidden: false,
204+
id: "gpt-test",
205+
isDefault: true,
206+
model: "gpt-test",
207+
supportedReasoningEfforts: [],
208+
});
209+
const models = [{ slug: "gpt-test", name: "GPT Test", isCustom: false, capabilities }];
210+
assert.strictEqual(codexDefaultCyberAccessProgram(models, "gpt-test"), "standard");
211+
assert.strictEqual(codexDefaultCyberAccessProgram(models, "my-model"), undefined);
212+
});
213+
214+
it("follows Codex's daybreak setting for the default program", () => {
215+
const capabilities = (cyber: ReadonlyArray<"standard" | "daybreakBlue">) =>
216+
mapCodexModelCapabilities({
217+
additionalSpeedTiers: [],
218+
availableAccessPrograms: { cyber: [...cyber] },
219+
defaultReasoningEffort: "medium",
220+
description: "Test model",
221+
displayName: "GPT Test",
222+
hidden: false,
223+
id: "gpt-test",
224+
isDefault: true,
225+
model: "gpt-test",
226+
supportedReasoningEfforts: [],
227+
});
228+
const models = [
229+
{
230+
slug: "both",
231+
name: "Both",
232+
isCustom: false,
233+
capabilities: capabilities(["standard", "daybreakBlue"]),
234+
},
235+
{
236+
slug: "standard",
237+
name: "Standard",
238+
isCustom: false,
239+
capabilities: capabilities(["standard"]),
240+
},
241+
];
242+
243+
const on = applyConfiguredCodexCyberAccess(models, {
244+
daybreakEnabled: true,
245+
apiKeyProgramsDisabled: false,
246+
});
247+
assert.strictEqual(codexDefaultCyberAccessProgram(on, "both"), "daybreakBlue");
248+
assert.strictEqual(codexDefaultCyberAccessProgram(on, "standard"), "standard");
249+
250+
const disabled = applyConfiguredCodexCyberAccess(models, {
251+
daybreakEnabled: false,
252+
apiKeyProgramsDisabled: true,
253+
});
254+
assert.strictEqual(codexDefaultCyberAccessProgram(disabled, "both"), undefined);
200255
});
201256

202257
it("keeps Daybreak access off custom models that borrow built-in options", () => {

‎apps/server/src/provider/CodexProvider.ts‎

Lines changed: 78 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ import {
5252
type CodexRateLimitSnapshot,
5353
type CodexResetCreditsSummary,
5454
} from "./codexUsageLimits.ts";
55-
import { CODEX_AUTOMATIC_CYBER_ACCESS_PROGRAM_ID } from "../codexModelOptions.ts";
5655
import * as HostProcess from "@t3tools/shared/HostProcess";
5756
import packageJson from "../../package.json" with { type: "json" };
5857
const isCodexAppServerSpawnError = Schema.is(CodexErrors.CodexAppServerSpawnError);
@@ -94,6 +93,8 @@ export interface CodexAppServerProviderSnapshot {
9493
readonly version: string | undefined;
9594
readonly models: ReadonlyArray<ServerProviderModel> | undefined;
9695
readonly serviceTier?: string | undefined;
96+
readonly daybreakEnabled?: boolean | undefined;
97+
readonly apiKeyCyberAccessProgramsDisabled?: boolean | undefined;
9798
readonly skills: ReadonlyArray<ServerProviderSkill>;
9899
}
99100

@@ -244,24 +245,21 @@ export function mapCodexModelCapabilities(
244245
});
245246
}
246247
const cyberAccessPrograms = model.availableAccessPrograms?.cyber ?? [];
247-
// Only Daybreak programmes need a choice; standard-only models keep their usual controls.
248-
if (cyberAccessPrograms.some((program) => program !== "standard")) {
248+
if (cyberAccessPrograms.length > 0) {
249+
// Codex's own clients request Standard unless Daybreak is on; omitting the field does not.
250+
const defaultProgram = cyberAccessPrograms.includes("standard")
251+
? "standard"
252+
: cyberAccessPrograms[0];
249253
optionDescriptors.push({
250254
id: "cyberAccessProgram",
251255
label: "Cyber Access",
252256
type: "select",
253-
options: [
254-
{
255-
id: CODEX_AUTOMATIC_CYBER_ACCESS_PROGRAM_ID,
256-
label: "Automatic",
257-
isDefault: true,
258-
},
259-
...cyberAccessPrograms.map((program) => ({
260-
id: program,
261-
label: CYBER_ACCESS_PROGRAM_LABELS[program] ?? program,
262-
})),
263-
],
264-
currentValue: CODEX_AUTOMATIC_CYBER_ACCESS_PROGRAM_ID,
257+
options: cyberAccessPrograms.map((program) => ({
258+
id: program,
259+
label: CYBER_ACCESS_PROGRAM_LABELS[program] ?? program,
260+
...(program === defaultProgram ? { isDefault: true } : {}),
261+
})),
262+
...(defaultProgram === undefined ? {} : { currentValue: defaultProgram }),
265263
});
266264
}
267265

@@ -303,6 +301,58 @@ export function applyPreferredCodexDefaultModel(
303301
});
304302
}
305303

304+
/** Mirrors Codex: `daybreak = true` prefers a Daybreak program, Blue before Red. */
305+
export function applyConfiguredCodexCyberAccess(
306+
models: ReadonlyArray<ServerProviderModel>,
307+
input: {
308+
readonly daybreakEnabled: boolean;
309+
readonly apiKeyProgramsDisabled: boolean;
310+
},
311+
): ReadonlyArray<ServerProviderModel> {
312+
if (!input.daybreakEnabled && !input.apiKeyProgramsDisabled) return models;
313+
return models.map((model) => {
314+
const descriptors = model.capabilities?.optionDescriptors;
315+
if (!model.capabilities || !descriptors) return model;
316+
return {
317+
...model,
318+
capabilities: {
319+
...model.capabilities,
320+
// Codex rejects explicit programs in this session, so none can be offered or sent.
321+
optionDescriptors: (input.apiKeyProgramsDisabled
322+
? descriptors.filter((descriptor) => descriptor.id !== "cyberAccessProgram")
323+
: descriptors
324+
).map((descriptor) => {
325+
if (descriptor.id !== "cyberAccessProgram" || descriptor.type !== "select") {
326+
return descriptor;
327+
}
328+
const value = ["daybreakBlue", "daybreakRed"].find((program) =>
329+
descriptor.options.some((option) => option.id === program),
330+
);
331+
if (!value) return descriptor;
332+
return {
333+
...descriptor,
334+
currentValue: value,
335+
options: descriptor.options.map(({ isDefault: _isDefault, ...option }) =>
336+
option.id === value ? { ...option, isDefault: true } : option,
337+
),
338+
};
339+
}),
340+
},
341+
};
342+
});
343+
}
344+
345+
/** Applies when a selection names no program, such as older threads and agent-started turns. */
346+
export function codexDefaultCyberAccessProgram(
347+
models: ReadonlyArray<ServerProviderModel>,
348+
slug: string,
349+
): string | undefined {
350+
const descriptor = models
351+
.find((model) => model.slug === slug)
352+
?.capabilities?.optionDescriptors?.find(({ id }) => id === "cyberAccessProgram");
353+
return descriptor?.type === "select" ? descriptor.currentValue : undefined;
354+
}
355+
306356
function applyConfiguredCodexServiceTier(
307357
models: ReadonlyArray<ServerProviderModel>,
308358
serviceTier: string | undefined,
@@ -580,7 +630,7 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun
580630

581631
// Optional discovery must finish before the outer timeout can discard the account result.
582632
const deadlineMs = spawnedAtMs + CODEX_AUTH_PROBE_TIMEOUT_MS - CODEX_DISCOVERY_MARGIN_MS;
583-
const [skillsResponse, models, serviceTier, rateLimits] = yield* Effect.all(
633+
const [skillsResponse, models, config, rateLimits] = yield* Effect.all(
584634
[
585635
withCodexDiscoveryDeadline(client.request("skills/list", { cwds: [input.cwd] }), deadlineMs),
586636
accountResponse.account?.type === "chatgpt"
@@ -597,7 +647,7 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun
597647
deadlineMs,
598648
CODEX_CONFIG_PROBE_TIMEOUT_MS,
599649
).pipe(
600-
Effect.map((response) => response?.config.service_tier ?? undefined),
650+
Effect.map((response) => response?.config),
601651
Effect.catch(() => Effect.logDebug("Codex config read failed.").pipe(Effect.as(undefined))),
602652
),
603653
// Usage is an enrichment: a failure or a slow answer degrades to "no
@@ -635,7 +685,12 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun
635685
...(rateLimits ? { rateLimits } : {}),
636686
version,
637687
models,
638-
serviceTier,
688+
serviceTier: config?.service_tier ?? undefined,
689+
daybreakEnabled: config?.["daybreak"] === true,
690+
apiKeyCyberAccessProgramsDisabled:
691+
accountResponse.account?.type === "apiKey" &&
692+
(config?.["features"] as { readonly api_key_cyber_access_programs?: unknown } | undefined)
693+
?.api_key_cyber_access_programs === false,
639694
skills: skillsResponse ? parseCodexSkillsListResponse(skillsResponse, input.cwd) : [],
640695
} satisfies CodexAppServerProviderSnapshot;
641696
});
@@ -893,6 +948,11 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
893948
appendCustomCodexModels(snapshot.models ?? [], codexSettings.customModels),
894949
applyPreferredCodexDefaultModel,
895950
(models) => applyConfiguredCodexServiceTier(models, snapshot.serviceTier),
951+
(models) =>
952+
applyConfiguredCodexCyberAccess(models, {
953+
daybreakEnabled: snapshot.daybreakEnabled === true,
954+
apiKeyProgramsDisabled: snapshot.apiKeyCyberAccessProgramsDisabled === true,
955+
}),
896956
),
897957
skills: snapshot.skills,
898958
slashCommands: [

0 commit comments

Comments
 (0)