Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions apps/server/src/provider/Layers/CodexAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,32 @@ validationLayer("CodexAdapterLive validation", (it) => {
});
}),
);

it.effect("passes configured profile names to Codex app-server sessions", () => {
const runtimeFactory = makeRuntimeFactory();
const layer = Layer.effect(
CodexAdapter,
makeCodexAdapter(decodeCodexSettings({ profileName: "work" }), {
makeRuntime: runtimeFactory.factory,
}),
).pipe(
Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),
Layer.provideMerge(ServerSettingsService.layerTest()),
Layer.provideMerge(providerSessionDirectoryTestLayer),
Layer.provideMerge(NodeServices.layer),
);

return Effect.gen(function* () {
const adapter = yield* CodexAdapter;
yield* adapter.startSession({
provider: ProviderDriverKind.make("codex"),
threadId: asThreadId("thread-profile"),
runtimeMode: "full-access",
});

assert.equal(runtimeFactory.factory.mock.calls[0]?.[0].profileName, "work");
}).pipe(Effect.provide(layer));
});
});

const sessionRuntimeFactory = makeRuntimeFactory();
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/provider/Layers/CodexAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1385,6 +1385,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
binaryPath: codexConfig.binaryPath,
...(options?.environment ? { environment: options.environment } : {}),
...(codexConfig.homePath ? { homePath: codexConfig.homePath } : {}),
...(codexConfig.profileName ? { profileName: codexConfig.profileName } : {}),
...(isCodexResumeCursorSchema(input.resumeCursor)
? { resumeCursor: input.resumeCursor }
: {}),
Expand Down
13 changes: 10 additions & 3 deletions apps/server/src/provider/Layers/CodexProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ export function buildCodexInitializeParams(): CodexSchema.V1InitializeParams {
const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(function* (input: {
readonly binaryPath: string;
readonly homePath?: string;
readonly profileName?: string;
readonly cwd: string;
readonly customModels?: ReadonlyArray<string>;
readonly environment?: NodeJS.ProcessEnv;
Expand All @@ -263,7 +264,7 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun
const clientContext = yield* Layer.build(
CodexClient.layerCommand({
command: input.binaryPath,
args: ["app-server"],
args: [...(input.profileName ? ["-p", input.profileName] : []), "app-server"],
cwd: input.cwd,
env: {
...(input.environment ?? process.env),
Expand Down Expand Up @@ -292,7 +293,7 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun
const version = versionMatch ? versionMatch[1] : undefined;

const accountResponse = yield* client.request("account/read", {});
if (!accountResponse.account && accountResponse.requiresOpenaiAuth) {
if (!input.profileName && !accountResponse.account && accountResponse.requiresOpenaiAuth) {
return {
account: accountResponse,
version,
Expand Down Expand Up @@ -404,6 +405,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
probe: (input: {
readonly binaryPath: string;
readonly homePath?: string;
readonly profileName?: string;
readonly cwd: string;
readonly customModels: ReadonlyArray<string>;
readonly environment?: NodeJS.ProcessEnv;
Expand Down Expand Up @@ -441,6 +443,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
const probeResult = yield* probe({
binaryPath: codexSettings.binaryPath,
homePath: codexSettings.homePath,
profileName: codexSettings.profileName,
cwd: process.cwd(),
customModels: codexSettings.customModels,
environment,
Expand Down Expand Up @@ -489,7 +492,11 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
}

const snapshot = probeResult.success.value;
const accountStatus = accountProbeStatus(snapshot.account);
const hasProviderModels = snapshot.models.some((model) => !model.isCustom);
const account = hasProviderModels
? { ...snapshot.account, requiresOpenaiAuth: false }
: snapshot.account;
const accountStatus = accountProbeStatus(account);
Comment thread
cursor[bot] marked this conversation as resolved.

return buildServerProvider({
presentation: CODEX_PRESENTATION,
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ describe("openCodexThread", () => {
runtimeMode: "full-access",
cwd: "/tmp/project",
requestedModel: "gpt-5.3-codex",
modelProvider: "codex-lb",
serviceTier: undefined,
resumeThreadId: "stale-thread",
}),
Expand All @@ -235,6 +236,7 @@ describe("openCodexThread", () => {
calls.map((call) => call.method),
["thread/resume", "thread/start"],
);
assert.equal((calls[0]!.payload as { modelProvider?: string }).modelProvider, "codex-lb");
});

it("propagates non-recoverable resume failures", async () => {
Expand Down Expand Up @@ -265,6 +267,7 @@ describe("openCodexThread", () => {
runtimeMode: "full-access",
cwd: "/tmp/project",
requestedModel: "gpt-5.3-codex",
modelProvider: undefined,
serviceTier: undefined,
resumeThreadId: "stale-thread",
}),
Expand Down
21 changes: 20 additions & 1 deletion apps/server/src/provider/Layers/CodexSessionRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export interface CodexSessionRuntimeOptions {
readonly providerInstanceId?: ProviderInstanceId;
readonly binaryPath: string;
readonly homePath?: string;
readonly profileName?: string;
readonly environment?: NodeJS.ProcessEnv;
readonly cwd: string;
readonly runtimeMode: RuntimeMode;
Expand Down Expand Up @@ -286,6 +287,7 @@ function buildThreadStartParams(input: {
readonly cwd: string;
readonly runtimeMode: RuntimeMode;
readonly model: string | undefined;
readonly modelProvider: string | undefined;
readonly serviceTier: CodexServiceTier | undefined;
}): EffectCodexSchema.V2ThreadStartParams {
const config = runtimeModeToThreadConfig(input.runtimeMode);
Expand All @@ -294,6 +296,7 @@ function buildThreadStartParams(input: {
approvalPolicy: config.approvalPolicy,
sandbox: config.sandbox,
...(input.model ? { model: input.model } : {}),
...(input.modelProvider ? { modelProvider: input.modelProvider } : {}),
...(input.serviceTier ? { serviceTier: input.serviceTier } : {}),
};
}
Expand Down Expand Up @@ -435,6 +438,7 @@ export const openCodexThread = (input: {
readonly runtimeMode: RuntimeMode;
readonly cwd: string;
readonly requestedModel: string | undefined;
readonly modelProvider: string | undefined;
readonly serviceTier: CodexServiceTier | undefined;
readonly resumeThreadId: string | undefined;
}): Effect.Effect<CodexThreadOpenResponse, CodexErrors.CodexAppServerError> => {
Expand All @@ -443,6 +447,7 @@ export const openCodexThread = (input: {
cwd: input.cwd,
runtimeMode: input.runtimeMode,
model: input.requestedModel,
modelProvider: input.modelProvider,
serviceTier: input.serviceTier,
});

Expand Down Expand Up @@ -717,9 +722,12 @@ export const makeCodexSessionRuntime = (
...(options.environment ?? process.env),
...(resolvedHomePath ? { CODEX_HOME: resolvedHomePath } : {}),
};
const appServerArgs = options.profileName
? ["-p", options.profileName, "app-server"]
: ["app-server"];
const child = yield* spawner
.spawn(
ChildProcess.make(options.binaryPath, ["app-server"], {
ChildProcess.make(options.binaryPath, appServerArgs, {
cwd: options.cwd,
env,
forceKillAfter: CODEX_APP_SERVER_FORCE_KILL_AFTER,
Expand Down Expand Up @@ -1183,13 +1191,24 @@ export const makeCodexSessionRuntime = (
yield* client.notify("initialized", undefined);

const requestedModel = normalizeCodexModelSlug(options.model);
const codexConfig = yield* client.request("config/read", { cwd: options.cwd }).pipe(
Effect.map((response) => response.config),
Effect.orElseSucceed(() => undefined),
);
const modelProvider =
(
(options.profileName
? codexConfig?.profiles?.[options.profileName]?.model_provider
: undefined) ?? codexConfig?.model_provider
)?.trim() || undefined;

const opened = yield* openCodexThread({
client,
threadId: options.threadId,
runtimeMode: options.runtimeMode,
cwd: options.cwd,
requestedModel,
modelProvider,
serviceTier: options.serviceTier,
resumeThreadId: readResumeCursorThreadId(options.resumeCursor),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const makeCodexConfig = (overrides: Partial<CodexSettings>): CodexSettings => ({
binaryPath: "codex",
homePath: "",
shadowHomePath: "",
profileName: "",
customModels: [],
...overrides,
});
Expand Down
62 changes: 62 additions & 0 deletions apps/server/src/provider/Layers/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
account: null,
requiresOpenaiAuth: true,
},
models: [],
}),
),
);
Expand All @@ -368,6 +369,52 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
}),
);

it.effect("keeps profiled Codex providers ready when models are available", () =>
Effect.gen(function* () {
const status = yield* checkCodexProviderStatus(
{ ...defaultCodexSettings, profileName: "work" },
() =>
Effect.succeed(
makeCodexProbeSnapshot({
account: {
account: null,
requiresOpenaiAuth: true,
},
}),
),
);

assert.strictEqual(status.status, "ready");
assert.strictEqual(status.auth.status, "unknown");
}),
);
Comment thread
cursor[bot] marked this conversation as resolved.

it.effect("keeps OpenAI auth required when only custom models are available", () =>
Effect.gen(function* () {
const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>
Effect.succeed(
makeCodexProbeSnapshot({
account: {
account: null,
requiresOpenaiAuth: true,
},
models: [
{
slug: "custom-model",
name: "custom-model",
isCustom: true,
capabilities: null,
},
],
}),
),
);

assert.strictEqual(status.status, "error");
assert.strictEqual(status.auth.status, "unauthenticated");
}),
);

it.effect(
"returns ready with unknown auth when app-server does not require OpenAI auth",
() =>
Expand All @@ -388,6 +435,21 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
}),
);

it.effect("passes the configured Codex profile to the app-server probe", () =>
Effect.gen(function* () {
let profileName: string | undefined;
yield* checkCodexProviderStatus(
{ ...defaultCodexSettings, profileName: "work" },
(input) => {
profileName = input.profileName;
return Effect.succeed(makeCodexProbeSnapshot());
},
);

assert.strictEqual(profileName, "work");
}),
);

it.effect("returns an api key label for codex api key auth", () =>
Effect.gen(function* () {
const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>
Expand Down
5 changes: 5 additions & 0 deletions apps/server/src/serverSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ it.layer(NodeServices.layer)("server settings", (it) => {
assert.deepEqual(decodePatch({ providers: { codex: { binaryPath: "/tmp/codex" } } }), {
providers: { codex: { binaryPath: "/tmp/codex" } },
});
assert.deepEqual(decodePatch({ providers: { codex: { profileName: "work" } } }), {
providers: { codex: { profileName: "work" } },
});

assert.deepEqual(
decodePatch({
Expand Down Expand Up @@ -118,6 +121,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
binaryPath: "/opt/homebrew/bin/codex",
homePath: "/Users/julius/.codex",
shadowHomePath: "",
profileName: "",
customModels: [],
});
assert.deepEqual(next.providers.claudeAgent, {
Expand Down Expand Up @@ -359,6 +363,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
binaryPath: "/opt/homebrew/bin/codex",
homePath: "",
shadowHomePath: "",
profileName: "",
customModels: [],
});
assert.deepEqual(next.providers.claudeAgent, {
Expand Down
23 changes: 22 additions & 1 deletion apps/server/src/textGeneration/CodexTextGeneration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function makeFakeCodexBinary(
requireImage?: boolean;
requireFastServiceTier?: boolean;
requireReasoningEffort?: string;
requireProfileName?: string;
forbidReasoningEffort?: boolean;
stdinMustContain?: string;
stdinMustNotContain?: string;
Expand All @@ -54,7 +55,14 @@ function makeFakeCodexBinary(
'seen_image="0"',
'seen_fast_service_tier="0"',
'seen_reasoning_effort=""',
'seen_profile_name=""',
"while [ $# -gt 0 ]; do",
' if [ "$1" = "-p" ]; then',
" shift",
' seen_profile_name="$1"',
" shift",
" continue",
" fi",
' if [ "$1" = "--image" ]; then',
" shift",
' if [ -n "$1" ]; then',
Expand Down Expand Up @@ -109,6 +117,14 @@ function makeFakeCodexBinary(
"fi",
]
: []),
...(input.requireProfileName !== undefined
? [
`if [ "$seen_profile_name" != "${input.requireProfileName}" ]; then`,
' printf "%s\\n" "unexpected profile name: $seen_profile_name" >&2',
` exit 8`,
"fi",
]
: []),
...(input.forbidReasoningEffort
? [
'if [ -n "$seen_reasoning_effort" ]; then',
Expand Down Expand Up @@ -163,6 +179,7 @@ function withFakeCodexEnv<A, E, R>(
requireImage?: boolean;
requireFastServiceTier?: boolean;
requireReasoningEffort?: string;
requireProfileName?: string;
forbidReasoningEffort?: boolean;
stdinMustContain?: string;
stdinMustNotContain?: string;
Expand All @@ -173,7 +190,10 @@ function withFakeCodexEnv<A, E, R>(
const fs = yield* FileSystem.FileSystem;
const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-codex-text-" });
const codexPath = yield* makeFakeCodexBinary(tempDir, input);
const config = decodeCodexSettings({ binaryPath: codexPath });
const config = decodeCodexSettings({
binaryPath: codexPath,
...(input.requireProfileName ? { profileName: input.requireProfileName } : {}),
});
const textGeneration = yield* makeCodexTextGeneration(config);
return yield* effectFn(textGeneration);
}).pipe(Effect.scoped);
Expand Down Expand Up @@ -219,6 +239,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {
}),
requireFastServiceTier: true,
requireReasoningEffort: "xhigh",
requireProfileName: "work",
stdinMustNotContain: "branch must be a short semantic git branch fragment",
},
(textGeneration) =>
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/textGeneration/CodexTextGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
const command = ChildProcess.make(
codexConfig.binaryPath || "codex",
[
...(codexConfig.profileName ? ["-p", codexConfig.profileName] : []),
"exec",
"--ephemeral",
"--skip-git-repo-check",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/KeybindingsToast.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ function createBaseServerConfig(): ServerConfig {
binaryPath: "",
homePath: "",
shadowHomePath: "",
profileName: "",
customModels: [],
},
claudeAgent: {
Expand Down
Loading
Loading