Skip to content
This repository was archived by the owner on Mar 10, 2026. It is now read-only.
Merged
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
14 changes: 14 additions & 0 deletions apps/server/src/codexAppServerManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,20 @@ describe("startSession", () => {
});
});

it("spoofs Codex Desktop initialize metadata when requested", () => {
expect(buildCodexInitializeParams({ spoofAsCodexDesktop: true })).toEqual({
clientInfo: {
name: "codex_desktop",
title: "Codex Desktop",
version: "26.305.950",
},
capabilities: {
experimentalApi: true,
optOutNotificationMethods: ["codex/event/session_configured"],
},
});
});

it("emits session/startFailed when resolving cwd throws before process launch", async () => {
const manager = new CodexAppServerManager();
const events: Array<{ method: string; kind: string; message?: string }> = [];
Expand Down
33 changes: 31 additions & 2 deletions apps/server/src/codexAppServerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ const RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS = [
const CODEX_DEFAULT_MODEL = "gpt-5.3-codex";
const CODEX_SPARK_MODEL = "gpt-5.3-codex-spark";
const CODEX_SPARK_DISABLED_PLAN_TYPES = new Set<CodexPlanType>(["free", "go", "plus"]);
const CODEX_DESKTOP_SERVICE_NAME = "codex_desktop";
const CODEX_DESKTOP_SPOOF_VERSION = "26.305.950";
const CODEX_DESKTOP_SPOOF_OPT_OUT_NOTIFICATION_METHODS = [
"codex/event/session_configured",
] as const;

function asObject(value: unknown): Record<string, unknown> | undefined {
if (!value || typeof value !== "object") {
Expand Down Expand Up @@ -401,7 +406,23 @@ export function normalizeCodexModelSlug(
return normalized;
}

export function buildCodexInitializeParams() {
export function buildCodexInitializeParams(input?: {
readonly spoofAsCodexDesktop?: boolean;
}) {
if (input?.spoofAsCodexDesktop) {
return {
clientInfo: {
name: CODEX_DESKTOP_SERVICE_NAME,
title: "Codex Desktop",
version: CODEX_DESKTOP_SPOOF_VERSION,
},
capabilities: {
experimentalApi: true,
optOutNotificationMethods: [...CODEX_DESKTOP_SPOOF_OPT_OUT_NOTIFICATION_METHODS],
},
} as const;
}

return {
clientInfo: {
name: "t3code_desktop",
Expand Down Expand Up @@ -543,6 +564,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
const codexOptions = readCodexProviderOptions(input);
const codexBinaryPath = codexOptions.binaryPath ?? "codex";
const codexHomePath = codexOptions.homePath;
const spoofAsCodexDesktop = codexOptions.spoofAsCodexDesktop === true;
this.assertSupportedCodexCliVersion({
binaryPath: codexBinaryPath,
cwd: resolvedCwd,
Expand Down Expand Up @@ -580,7 +602,11 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve

this.emitLifecycleEvent(context, "session/connecting", "Starting codex app-server");

await this.sendRequest(context, "initialize", buildCodexInitializeParams());
await this.sendRequest(
context,
"initialize",
buildCodexInitializeParams({ spoofAsCodexDesktop }),
);

this.writeMessage(context, { method: "initialized" });
try {
Expand Down Expand Up @@ -610,6 +636,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
model: normalizedModel ?? null,
...(input.serviceTier !== undefined ? { serviceTier: input.serviceTier } : {}),
cwd: input.cwd ?? null,
...(spoofAsCodexDesktop ? { serviceName: CODEX_DESKTOP_SERVICE_NAME } : {}),
...mapCodexRuntimeMode(input.runtimeMode ?? "full-access"),
};

Expand Down Expand Up @@ -1510,6 +1537,7 @@ function normalizeProviderThreadId(value: string | undefined): string | undefine
function readCodexProviderOptions(input: CodexAppServerStartSessionInput): {
readonly binaryPath?: string;
readonly homePath?: string;
readonly spoofAsCodexDesktop?: boolean;
} {
const options = input.providerOptions?.codex;
if (!options) {
Expand All @@ -1518,6 +1546,7 @@ function readCodexProviderOptions(input: CodexAppServerStartSessionInput): {
return {
...(options.binaryPath ? { binaryPath: options.binaryPath } : {}),
...(options.homePath ? { homePath: options.homePath } : {}),
...(options.spoofAsCodexDesktop === true ? { spoofAsCodexDesktop: true } : {}),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,47 @@ describe("ProviderCommandReactor", () => {
});
});

it("forwards codex provider options through session start", async () => {
const harness = await createHarness();
const now = new Date().toISOString();

await Effect.runPromise(
harness.engine.dispatch({
type: "thread.turn.start",
commandId: CommandId.makeUnsafe("cmd-turn-start-provider-options"),
threadId: ThreadId.makeUnsafe("thread-1"),
message: {
messageId: asMessageId("user-message-provider-options"),
role: "user",
text: "hello desktop spoof",
attachments: [],
},
provider: "codex",
providerOptions: {
codex: {
binaryPath: "/usr/local/bin/codex",
homePath: "/tmp/.codex",
spoofAsCodexDesktop: true,
},
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
createdAt: now,
}),
);

await waitFor(() => harness.startSession.mock.calls.length === 1);
expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({
providerOptions: {
codex: {
binaryPath: "/usr/local/bin/codex",
homePath: "/tmp/.codex",
spoofAsCodexDesktop: true,
},
},
});
});

it("forwards plan interaction mode to the provider turn request", async () => {
const harness = await createHarness();
const now = new Date().toISOString();
Expand Down
10 changes: 10 additions & 0 deletions apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type OrchestrationEvent,
type ProviderModelOptions,
type ProviderKind,
type ProviderStartOptions,
type ProviderServiceTier,
type OrchestrationSession,
ThreadId,
Expand Down Expand Up @@ -203,6 +204,7 @@ const make = Effect.gen(function* () {
readonly provider?: ProviderKind;
readonly model?: string;
readonly modelOptions?: ProviderModelOptions;
readonly providerOptions?: ProviderStartOptions;
readonly serviceTier?: ProviderServiceTier | null;
},
) {
Expand Down Expand Up @@ -242,6 +244,9 @@ const make = Effect.gen(function* () {
...(desiredModel ? { model: desiredModel } : {}),
...(options?.serviceTier !== undefined ? { serviceTier: options.serviceTier } : {}),
...(options?.modelOptions !== undefined ? { modelOptions: options.modelOptions } : {}),
...(options?.providerOptions !== undefined
? { providerOptions: options.providerOptions }
: {}),
...(input?.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}),
runtimeMode: desiredRuntimeMode,
});
Expand Down Expand Up @@ -328,6 +333,7 @@ const make = Effect.gen(function* () {
readonly model?: string;
readonly serviceTier?: ProviderServiceTier | null;
readonly modelOptions?: ProviderModelOptions;
readonly providerOptions?: ProviderStartOptions;
readonly interactionMode?: "default" | "plan";
readonly createdAt: string;
}) {
Expand All @@ -340,6 +346,7 @@ const make = Effect.gen(function* () {
...(input.model !== undefined ? { model: input.model } : {}),
...(input.serviceTier !== undefined ? { serviceTier: input.serviceTier } : {}),
...(input.modelOptions !== undefined ? { modelOptions: input.modelOptions } : {}),
...(input.providerOptions !== undefined ? { providerOptions: input.providerOptions } : {}),
});
const normalizedInput = toNonEmptyProviderInput(input.messageText);
const normalizedAttachments = input.attachments ?? [];
Expand Down Expand Up @@ -475,6 +482,9 @@ const make = Effect.gen(function* () {
...(event.payload.model !== undefined ? { model: event.payload.model } : {}),
...(event.payload.serviceTier !== undefined ? { serviceTier: event.payload.serviceTier } : {}),
...(event.payload.modelOptions !== undefined ? { modelOptions: event.payload.modelOptions } : {}),
...(event.payload.providerOptions !== undefined
? { providerOptions: event.payload.providerOptions }
: {}),
interactionMode: event.payload.interactionMode,
createdAt: event.payload.createdAt,
});
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/orchestration/decider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,9 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
...(command.model !== undefined ? { model: command.model } : {}),
...(command.serviceTier !== undefined ? { serviceTier: command.serviceTier } : {}),
...(command.modelOptions !== undefined ? { modelOptions: command.modelOptions } : {}),
...(command.providerOptions !== undefined
? { providerOptions: command.providerOptions }
: {}),
assistantDeliveryMode: command.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE,
runtimeMode:
readModel.threads.find((entry) => entry.id === command.threadId)?.runtimeMode ??
Expand Down
33 changes: 33 additions & 0 deletions apps/server/src/provider/Layers/CodexAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,39 @@ validationLayer("CodexAdapterLive validation", (it) => {
});
}),
);

it.effect("passes codex provider options through session start", () =>
Effect.gen(function* () {
validationManager.startSessionImpl.mockClear();
const adapter = yield* CodexAdapter;

yield* adapter.startSession({
provider: "codex",
threadId: asThreadId("thread-1"),
runtimeMode: "full-access",
providerOptions: {
codex: {
binaryPath: "/usr/local/bin/codex",
homePath: "/tmp/.codex",
spoofAsCodexDesktop: true,
},
},
});

assert.deepStrictEqual(validationManager.startSessionImpl.mock.calls[0]?.[0], {
provider: "codex",
threadId: asThreadId("thread-1"),
providerOptions: {
codex: {
binaryPath: "/usr/local/bin/codex",
homePath: "/tmp/.codex",
spoofAsCodexDesktop: true,
},
},
runtimeMode: "full-access",
});
}),
);
});

const sessionErrorManager = new FakeCodexManager();
Expand Down
29 changes: 29 additions & 0 deletions apps/web/src/appSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getAppModelOptions,
getSlashModelOptions,
normalizeCustomModelSlugs,
resolveCodexProviderOptionsFromAppSettings,
resolveAppServiceTier,
shouldShowFastTierIcon,
resolveAppModelSelection,
Expand Down Expand Up @@ -96,6 +97,34 @@ describe("resolveAppServiceTier", () => {
});
});

describe("resolveCodexProviderOptionsFromAppSettings", () => {
it("omits codex provider options when no overrides are enabled", () => {
expect(
resolveCodexProviderOptionsFromAppSettings({
codexBinaryPath: "",
codexHomePath: " ",
spoofT3CodeAsCodexDesktop: false,
}),
).toBeUndefined();
});

it("collects codex path overrides and spoofing into provider options", () => {
expect(
resolveCodexProviderOptionsFromAppSettings({
codexBinaryPath: " /usr/local/bin/codex ",
codexHomePath: " /tmp/.codex ",
spoofT3CodeAsCodexDesktop: true,
}),
).toEqual({
codex: {
binaryPath: "/usr/local/bin/codex",
homePath: "/tmp/.codex",
spoofAsCodexDesktop: true,
},
});
});
});

describe("shouldShowFastTierIcon", () => {
it("shows the fast-tier icon only for gpt-5.4 on fast tier", () => {
expect(shouldShowFastTierIcon("gpt-5.4", "fast")).toBe(true);
Expand Down
32 changes: 31 additions & 1 deletion apps/web/src/appSettings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { useCallback, useSyncExternalStore } from "react";
import { Option, Schema } from "effect";
import { type ProviderKind, type ProviderServiceTier } from "@t3tools/contracts";
import {
type ProviderKind,
type ProviderServiceTier,
type ProviderStartOptions,
} from "@t3tools/contracts";
import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model";
import { PROVIDER_ORDER } from "@t3tools/shared/provider";

Expand Down Expand Up @@ -44,6 +48,9 @@ const AppSettingsSchema = Schema.Struct({
codexHomePath: Schema.String.check(Schema.isMaxLength(4096)).pipe(
Schema.withConstructorDefault(() => Option.some("")),
),
spoofT3CodeAsCodexDesktop: Schema.Boolean.pipe(
Schema.withConstructorDefault(() => Option.some(false)),
),
confirmThreadDelete: Schema.Boolean.pipe(Schema.withConstructorDefault(() => Option.some(true))),
enableAssistantStreaming: Schema.Boolean.pipe(
Schema.withConstructorDefault(() => Option.some(false)),
Expand Down Expand Up @@ -109,6 +116,29 @@ export function resolveAppServiceTier(serviceTier: AppServiceTier): ProviderServ
return serviceTier === "auto" ? null : serviceTier;
}

export function resolveCodexProviderOptionsFromAppSettings(
settings: Pick<
AppSettings,
"codexBinaryPath" | "codexHomePath" | "spoofT3CodeAsCodexDesktop"
>,
): ProviderStartOptions | undefined {
const binaryPath = settings.codexBinaryPath.trim();
const homePath = settings.codexHomePath.trim();
const spoofAsCodexDesktop = settings.spoofT3CodeAsCodexDesktop;

if (!binaryPath && !homePath && !spoofAsCodexDesktop) {
return undefined;
}

return {
codex: {
...(binaryPath ? { binaryPath } : {}),
...(homePath ? { homePath } : {}),
...(spoofAsCodexDesktop ? { spoofAsCodexDesktop: true } : {}),
},
};
}

export function shouldShowFastTierIcon(
model: string | null | undefined,
serviceTier: AppServiceTier,
Expand Down
Loading
Loading