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
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { makeProviderServiceLive } from "../src/provider/Layers/ProviderService.
import { makeCodexAdapterLive } from "../src/provider/Layers/CodexAdapter.ts";
import { CodexAdapter } from "../src/provider/Services/CodexAdapter.ts";
import { ProviderService } from "../src/provider/Services/ProviderService.ts";
import { CodexOpenAiEnvOverridesLive } from "../src/provider/Services/CodexOpenAiEnvOverrides.ts";
import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts";
import { CheckpointReactorLive } from "../src/orchestration/Layers/CheckpointReactor.ts";
import { OrchestrationEngineLive } from "../src/orchestration/Layers/OrchestrationEngine.ts";
Expand Down Expand Up @@ -263,6 +264,7 @@ export const makeOrchestrationIntegrationHarness = (
Layer.provide(makeCodexAdapterLive()),
Layer.provideMerge(ServerConfig.layerTest(workspaceDir, stateDir)),
Layer.provideMerge(NodeServices.layer),
Layer.provideMerge(CodexOpenAiEnvOverridesLive),
Layer.provideMerge(providerSessionDirectoryLayer),
);
const providerLayer = useRealCodex
Expand Down Expand Up @@ -391,7 +393,9 @@ export const makeOrchestrationIntegrationHarness = (
) =>
waitFor(
pendingApprovalRepository
.getByRequestId({ requestId: ApprovalRequestId.makeUnsafe(requestId) })
.getByRequestId({
requestId: ApprovalRequestId.makeUnsafe(requestId),
})
.pipe(
Effect.map((row) =>
Option.match(row, {
Expand Down
27 changes: 25 additions & 2 deletions apps/server/src/codexAppServerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ export interface CodexAppServerStartSessionInput {
readonly serviceTier?: string;
readonly resumeCursor?: unknown;
readonly providerOptions?: ProviderSessionStartInput["providerOptions"];
readonly openaiApiKey?: string;
readonly openaiBaseUrl?: string;
readonly runtimeMode: RuntimeMode;
}

Expand Down Expand Up @@ -376,7 +378,9 @@ export function resolveCodexModelForAccount(
function killChildTree(child: ChildProcessWithoutNullStreams): void {
if (process.platform === "win32" && child.pid !== undefined) {
try {
spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
stdio: "ignore",
});
return;
} catch {
// fallback to direct kill
Expand Down Expand Up @@ -541,6 +545,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
};

const codexOptions = readCodexProviderOptions(input);
const openAiOverrides = readCodexOpenAiEnvOverrides(input);
const codexBinaryPath = codexOptions.binaryPath ?? "codex";
const codexHomePath = codexOptions.homePath;
this.assertSupportedCodexCliVersion({
Expand All @@ -553,6 +558,10 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
env: {
...process.env,
...(codexHomePath ? { CODEX_HOME: codexHomePath } : {}),
...(openAiOverrides.openaiApiKey ? { OPENAI_API_KEY: openAiOverrides.openaiApiKey } : {}),
...(openAiOverrides.openaiBaseUrl
? { OPENAI_BASE_URL: openAiOverrides.openaiBaseUrl }
: {}),
},
stdio: ["pipe", "pipe", "pipe"],
shell: process.platform === "win32",
Expand Down Expand Up @@ -1145,7 +1154,9 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
this.readString(this.readObject(notification.params)?.thread, "id"),
);
if (providerThreadId) {
this.updateSession(context, { resumeCursor: { threadId: providerThreadId } });
this.updateSession(context, {
resumeCursor: { threadId: providerThreadId },
});
}
return;
}
Expand Down Expand Up @@ -1521,6 +1532,18 @@ function readCodexProviderOptions(input: CodexAppServerStartSessionInput): {
};
}

function readCodexOpenAiEnvOverrides(input: CodexAppServerStartSessionInput): {
readonly openaiApiKey?: string;
readonly openaiBaseUrl?: string;
} {
const openaiApiKey = input.openaiApiKey?.trim();
const openaiBaseUrl = input.openaiBaseUrl?.trim();
return {
...(openaiApiKey ? { openaiApiKey } : {}),
...(openaiBaseUrl ? { openaiBaseUrl } : {}),
};
}

function assertSupportedCodexCliVersion(input: {
readonly binaryPath: string;
readonly cwd: string;
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import { version } from "../package.json" with { type: "json" };
import { ServerLive } from "./wsServer";
import { NetService } from "@t3tools/shared/Net";
import { FetchHttpClient } from "effect/unstable/http";
import { CodexOpenAiEnvOverridesLive } from "./provider/Services/CodexOpenAiEnvOverrides";

const RuntimeLayer = Layer.empty.pipe(
Layer.provideMerge(CliConfig.layer),
Layer.provideMerge(ServerLive),
Layer.provideMerge(OpenLive),
Layer.provideMerge(NetService.layer),
Layer.provideMerge(CodexOpenAiEnvOverridesLive),
Layer.provideMerge(NodeServices.layer),
Layer.provideMerge(FetchHttpClient.layer),
);
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Open, type OpenShape } from "./open";
import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery";
import { AnalyticsService } from "./telemetry/Services/AnalyticsService";
import { Server, type ServerShape } from "./wsServer";
import { CodexOpenAiEnvOverridesLive } from "./provider/Services/CodexOpenAiEnvOverrides";

const start = vi.fn(() => undefined);
const stop = vi.fn(() => undefined);
Expand Down Expand Up @@ -52,6 +53,7 @@ const testLayer = Layer.mergeAll(
openInEditor: () => Effect.void,
} satisfies OpenShape),
AnalyticsService.layerTest,
CodexOpenAiEnvOverridesLive,
FetchHttpClient.layer,
NodeServices.layer,
);
Expand Down
11 changes: 9 additions & 2 deletions apps/server/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { fixPath, resolveStateDir } from "./os-jank";
import { Open } from "./open";
import * as SqlitePersistence from "./persistence/Layers/Sqlite";
import { makeServerProviderLayer, makeServerRuntimeServicesLayer } from "./serverLayers";
import { CodexOpenAiEnvOverridesLive } from "./provider/Services/CodexOpenAiEnvOverrides";
import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery";
import { ProviderHealthLive } from "./provider/Layers/ProviderHealth";
import { Server } from "./wsServer";
Expand Down Expand Up @@ -134,7 +135,10 @@ const ServerConfigLive = (input: CliInput) =>
const env = yield* CliEnvConfig.asEffect().pipe(
Effect.mapError(
(cause) =>
new StartupError({ message: "Failed to read environment configuration", cause }),
new StartupError({
message: "Failed to read environment configuration",
cause,
}),
),
);

Expand Down Expand Up @@ -196,6 +200,7 @@ const ServerConfigLive = (input: CliInput) =>
const LayerLive = (input: CliInput) =>
Layer.empty.pipe(
Layer.provideMerge(makeServerRuntimeServicesLayer()),
Layer.provideMerge(CodexOpenAiEnvOverridesLive),
Layer.provideMerge(makeServerProviderLayer()),
Layer.provideMerge(ProviderHealthLive),
Layer.provideMerge(SqlitePersistence.layerConfig),
Expand All @@ -220,7 +225,9 @@ export const recordStartupHeartbeat = Effect.gen(function* () {
projectCount: snapshot.projects.length,
})),
Effect.catch((cause) =>
Effect.logWarning("failed to gather startup snapshot for telemetry", { cause }).pipe(
Effect.logWarning("failed to gather startup snapshot for telemetry", {
cause,
}).pipe(
Effect.as({
threadCount: 0,
projectCount: 0,
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/provider/Layers/CodexAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from "../../codexAppServerManager.ts";
import { ServerConfig } from "../../config.ts";
import { CodexAdapter } from "../Services/CodexAdapter.ts";
import { CodexOpenAiEnvOverridesLive } from "../Services/CodexOpenAiEnvOverrides.ts";
import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts";
import { makeCodexAdapterLive } from "./CodexAdapter.ts";

Expand Down Expand Up @@ -151,6 +152,7 @@ const validationLayer = it.layer(
makeCodexAdapterLive({ manager: validationManager }).pipe(
Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),
Layer.provideMerge(providerSessionDirectoryTestLayer),
Layer.provideMerge(CodexOpenAiEnvOverridesLive),
Layer.provideMerge(NodeServices.layer),
),
);
Expand Down Expand Up @@ -192,6 +194,7 @@ const sessionErrorLayer = it.layer(
makeCodexAdapterLive({ manager: sessionErrorManager }).pipe(
Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),
Layer.provideMerge(providerSessionDirectoryTestLayer),
Layer.provideMerge(CodexOpenAiEnvOverridesLive),
Layer.provideMerge(NodeServices.layer),
),
);
Expand Down Expand Up @@ -259,6 +262,7 @@ const lifecycleLayer = it.layer(
makeCodexAdapterLive({ manager: lifecycleManager }).pipe(
Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),
Layer.provideMerge(providerSessionDirectoryTestLayer),
Layer.provideMerge(CodexOpenAiEnvOverridesLive),
Layer.provideMerge(NodeServices.layer),
),
);
Expand Down
64 changes: 36 additions & 28 deletions apps/server/src/provider/Layers/CodexAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
} from "../../codexAppServerManager.ts";
import { resolveAttachmentPath } from "../../attachmentStore.ts";
import { ServerConfig } from "../../config.ts";
import { CodexOpenAiEnvOverrides } from "../Services/CodexOpenAiEnvOverrides.ts";
import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";

const PROVIDER = "codex" as const;
Expand Down Expand Up @@ -1261,6 +1262,7 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const serverConfig = yield* Effect.service(ServerConfig);
const codexOpenAiEnvOverrides = yield* CodexOpenAiEnvOverrides;
const nativeEventLogger =
options?.nativeEventLogger ??
(options?.nativeEventLogPath !== undefined
Expand All @@ -1287,39 +1289,45 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) =>
}),
);

const startSession: CodexAdapterShape["startSession"] = (input) => {
if (input.provider !== undefined && input.provider !== PROVIDER) {
return Effect.fail(
new ProviderAdapterValidationError({
const startSession: CodexAdapterShape["startSession"] = (input) =>
Effect.gen(function* () {
if (input.provider !== undefined && input.provider !== PROVIDER) {
return yield* new ProviderAdapterValidationError({
provider: PROVIDER,
operation: "startSession",
issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`,
}),
);
}
});
}

const managerInput: CodexAppServerStartSessionInput = {
threadId: input.threadId,
provider: "codex",
...(input.cwd !== undefined ? { cwd: input.cwd } : {}),
...(input.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}),
...(input.providerOptions !== undefined ? { providerOptions: input.providerOptions } : {}),
runtimeMode: input.runtimeMode,
...(input.model !== undefined ? { model: input.model } : {}),
...(input.modelOptions?.codex?.fastMode ? { serviceTier: "fast" } : {}),
};
const openAiOverrides = yield* codexOpenAiEnvOverrides.get;
const managerInput: CodexAppServerStartSessionInput = {
threadId: input.threadId,
provider: "codex",
...(input.cwd !== undefined ? { cwd: input.cwd } : {}),
...(input.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}),
...(input.providerOptions !== undefined
? { providerOptions: input.providerOptions }
: {}),
...(openAiOverrides.openaiApiKey ? { openaiApiKey: openAiOverrides.openaiApiKey } : {}),
...(openAiOverrides.openaiBaseUrl
? { openaiBaseUrl: openAiOverrides.openaiBaseUrl }
: {}),
runtimeMode: input.runtimeMode,
...(input.model !== undefined ? { model: input.model } : {}),
...(input.modelOptions?.codex?.fastMode ? { serviceTier: "fast" } : {}),
};

return Effect.tryPromise({
try: () => manager.startSession(managerInput),
catch: (cause) =>
new ProviderAdapterProcessError({
provider: PROVIDER,
threadId: input.threadId,
detail: toMessage(cause, "Failed to start Codex adapter session."),
cause,
}),
}).pipe(Effect.map((session) => session));
};
return yield* Effect.tryPromise({
try: () => manager.startSession(managerInput),
catch: (cause) =>
new ProviderAdapterProcessError({
provider: PROVIDER,
threadId: input.threadId,
detail: toMessage(cause, "Failed to start Codex adapter session."),
cause,
}),
}).pipe(Effect.map((session) => session));
});

const sendTurn: CodexAdapterShape["sendTurn"] = (input) =>
Effect.gen(function* () {
Expand Down
43 changes: 43 additions & 0 deletions apps/server/src/provider/Services/CodexOpenAiEnvOverrides.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Effect, Layer, Ref, ServiceMap } from "effect";
import type { CodexSetOpenAiEnvResult } from "@t3tools/contracts";

export interface CodexOpenAiEnvOverridesState {
readonly openaiApiKey: string | null;
readonly openaiBaseUrl: string | null;
}

export interface CodexOpenAiEnvOverridesShape {
readonly get: Effect.Effect<CodexOpenAiEnvOverridesState>;
readonly set: (
input: Readonly<CodexOpenAiEnvOverridesState>,
) => Effect.Effect<CodexSetOpenAiEnvResult>;
}

export class CodexOpenAiEnvOverrides extends ServiceMap.Service<
CodexOpenAiEnvOverrides,
CodexOpenAiEnvOverridesShape
>()("t3/provider/Services/CodexOpenAiEnvOverrides") {}

export const CodexOpenAiEnvOverridesLive = Layer.effect(
CodexOpenAiEnvOverrides,
Effect.gen(function* () {
const ref = yield* Ref.make<CodexOpenAiEnvOverridesState>({
openaiApiKey: null,
openaiBaseUrl: null,
});

const get: CodexOpenAiEnvOverridesShape["get"] = Ref.get(ref);
const set: CodexOpenAiEnvOverridesShape["set"] = (input) =>
Ref.set(ref, {
openaiApiKey: input.openaiApiKey,
openaiBaseUrl: input.openaiBaseUrl,
}).pipe(
Effect.as({
openaiApiKeySet: Boolean(input.openaiApiKey && input.openaiApiKey.trim().length > 0),
openaiBaseUrl: input.openaiBaseUrl,
}),
);

return { get, set } satisfies CodexOpenAiEnvOverridesShape;
}),
);
7 changes: 6 additions & 1 deletion apps/server/src/serverLayers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { makeProviderServiceLive } from "./provider/Layers/ProviderService";
import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory";
import { ProviderService } from "./provider/Services/ProviderService";
import { makeEventNdjsonLogger } from "./provider/Layers/EventNdjsonLogger";
import { CodexOpenAiEnvOverrides } from "./provider/Services/CodexOpenAiEnvOverrides";

import { TerminalManagerLive } from "./terminal/Layers/Manager";
import { KeybindingsLive } from "./keybindings";
Expand All @@ -40,7 +41,11 @@ import { AnalyticsService } from "./telemetry/Services/AnalyticsService";
export function makeServerProviderLayer(): Layer.Layer<
ProviderService,
ProviderUnsupportedError,
SqlClient.SqlClient | ServerConfig | FileSystem.FileSystem | AnalyticsService
| SqlClient.SqlClient
| ServerConfig
| FileSystem.FileSystem
| AnalyticsService
| CodexOpenAiEnvOverrides
> {
return Effect.gen(function* () {
const { stateDir } = yield* ServerConfig;
Expand Down
Loading