Skip to content
Closed
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
123 changes: 123 additions & 0 deletions apps/server/src/codexAppServerManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { ApprovalRequestId, ThreadId } from "@peakcode/contracts";

import {
buildCodexProcessEnv,
configurePeakCodeGatewayProviderInCodexConfig,
disablePeakCodeBrowserPluginInCodexConfig,
removePeakCodeGatewayProviderFromCodexConfig,
resolveCodexBrowserUsePipePath,
} from "./codexProcessEnv";
import {
Expand Down Expand Up @@ -400,6 +402,127 @@ describe("buildCodexProcessEnv", () => {
'[plugins."peakcode-browser@local"]\nenabled = false',
);
});

it("configures Peak Code gateway as a Codex custom model provider", () => {
const config = configurePeakCodeGatewayProviderInCodexConfig(
[
'model = "gpt-5.5"',
'model_provider = "openai"',
"",
"[model_providers.openai]",
'name = "OpenAI"',
].join("\n"),
{
baseUrl: "http://127.0.0.1:3773/gateway/openai/v1",
apiKey: "local-token",
},
);

expect(config).toContain('model_provider = "peakcode-gateway"');
expect(config).toContain("[model_providers.openai]");
expect(config).toContain("[model_providers.peakcode-gateway]");
expect(config).toContain('name = "PeakCode Gateway"');
expect(config).toContain('base_url = "http://127.0.0.1:3773/gateway/openai/v1"');
expect(config).toContain('env_key = "PEAKCODE_GATEWAY_API_KEY"');
expect(config).not.toContain('model_provider = "openai"');
});

it("removes Peak Code gateway provider config when a normal Codex session is launched", () => {
const config = removePeakCodeGatewayProviderFromCodexConfig(
[
'model = "gpt-5.5"',
'model_provider = "peakcode-gateway"',
"",
"[model_providers.peakcode-gateway]",
'name = "PeakCode Gateway"',
'base_url = "http://127.0.0.1:3773/gateway/openai/v1"',
'env_key = "PEAKCODE_GATEWAY_API_KEY"',
"",
'[plugins."browser@openai-bundled"]',
"enabled = true",
].join("\n"),
);

expect(config).toContain('model = "gpt-5.5"');
expect(config).toContain('[plugins."browser@openai-bundled"]');
expect(config).not.toContain('model_provider = "peakcode-gateway"');
expect(config).not.toContain("[model_providers.peakcode-gateway]");
expect(config).not.toContain("PEAKCODE_GATEWAY_API_KEY");
});

it("writes gateway provider config into Peak Code's Codex home overlay", () => {
const tempDir = mkdtempSync(path.join(os.tmpdir(), "t3-codex-env-"));
const runtimeHome = mkdtempSync(path.join(os.tmpdir(), "t3-runtime-home-"));
try {
writeFileSync(path.join(tempDir, "config.toml"), 'model_provider = "openai"\n', "utf8");

const env = buildCodexProcessEnv({
env: { PEAKCODE_HOME: runtimeHome },
homePath: tempDir,
platform: "darwin",
gatewayProvider: {
baseUrl: "http://127.0.0.1:3773/gateway/openai/v1",
apiKey: "local-token",
},
});

expect(env.CODEX_HOME).toBe(path.join(runtimeHome, "codex-home-overlay"));
expect(env.PEAKCODE_GATEWAY_API_KEY).toBe("local-token");
const codexHome = env.CODEX_HOME;
if (typeof codexHome !== "string") {
throw new Error("Expected CODEX_HOME to be set.");
}
const overlayConfig = readFileSync(path.join(codexHome, "config.toml"), "utf8");
expect(overlayConfig).toContain('model_provider = "peakcode-gateway"');
expect(overlayConfig).toContain('name = "PeakCode Gateway"');
expect(overlayConfig).toContain(
'base_url = "http://127.0.0.1:3773/gateway/openai/v1"',
);
expect(overlayConfig).toContain('[plugins."peakcode-browser@local"]\nenabled = false');
} finally {
rmSync(tempDir, { recursive: true, force: true });
rmSync(runtimeHome, { recursive: true, force: true });
}
});

it("rewrites a stale gateway overlay back to normal Codex provider config", () => {
const tempDir = mkdtempSync(path.join(os.tmpdir(), "t3-codex-env-"));
const runtimeHome = mkdtempSync(path.join(os.tmpdir(), "t3-runtime-home-"));
try {
writeFileSync(
path.join(tempDir, "config.toml"),
[
'model = "gpt-5.5"',
'model_provider = "peakcode-gateway"',
"",
"[model_providers.peakcode-gateway]",
'name = "PeakCode Gateway"',
'base_url = "http://127.0.0.1:3773/gateway/openai/v1"',
'env_key = "PEAKCODE_GATEWAY_API_KEY"',
].join("\n"),
"utf8",
);

const env = buildCodexProcessEnv({
env: { PEAKCODE_HOME: runtimeHome },
homePath: tempDir,
platform: "darwin",
});

const codexHome = env.CODEX_HOME;
if (typeof codexHome !== "string") {
throw new Error("Expected CODEX_HOME to be set.");
}
const overlayConfig = readFileSync(path.join(codexHome, "config.toml"), "utf8");
expect(overlayConfig).toContain('model = "gpt-5.5"');
expect(overlayConfig).not.toContain('model_provider = "peakcode-gateway"');
expect(overlayConfig).not.toContain("[model_providers.peakcode-gateway]");
expect(overlayConfig).toContain('[plugins."peakcode-browser@local"]\nenabled = false');
} finally {
rmSync(tempDir, { recursive: true, force: true });
rmSync(runtimeHome, { recursive: true, force: true });
}
});
});

describe("handleStdoutLine", () => {
Expand Down
58 changes: 58 additions & 0 deletions apps/server/src/codexAppServerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
} from "./provider/codexCliVersion";
import { isNonFatalCodexErrorMessage } from "./codexErrorClassification.ts";
import { buildCodexProcessEnv } from "./codexProcessEnv.ts";
import { ServerConfig, type ServerConfigShape } from "./config.ts";
import { transcribeVoiceWithChatGptSession } from "./voiceTranscription.ts";

type PendingRequestKey = string;
Expand Down Expand Up @@ -233,6 +234,13 @@ const CODEX_DEFAULT_MODEL = "gpt-5.5";
const CODEX_SPARK_MODEL = "gpt-5.3-codex-spark";
const CODEX_SPARK_DISABLED_PLAN_TYPES = new Set<CodexPlanType>(["free", "go", "plus"]);
const CODEX_DISCOVERY_SESSION_IDLE_MS = 10 * 60 * 1000;
const DEEPSEEK_GATEWAY_CODEX_MODELS = new Set([
"deepseek",
"deepseek-chat",
"deepseek-reasoner",
"deepseek-v4-flash",
"deepseek-v4-pro",
]);

function asObject(value: unknown): Record<string, unknown> | undefined {
if (!value || typeof value !== "object") {
Expand All @@ -249,6 +257,24 @@ function normalizeCodexProcessLine(rawLine: string): string {
return rawLine.replaceAll(ANSI_ESCAPE_REGEX, "").trim();
}

function resolveDeepSeekGatewayCodexModel(model: string | undefined | null): string | undefined {
const trimmed = model?.trim();
if (!trimmed) return undefined;
const slashIndex = trimmed.indexOf("/");
const unprefixed =
slashIndex > 0 && trimmed.slice(0, slashIndex) === "deepseek"
? trimmed.slice(slashIndex + 1)
: trimmed;
return DEEPSEEK_GATEWAY_CODEX_MODELS.has(unprefixed) ? unprefixed : undefined;
}

function localGatewayBaseUrl(config: ServerConfigShape): string {
const host =
config.host && config.host !== "0.0.0.0" && config.host !== "::" ? config.host : "127.0.0.1";
const formattedHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
return `http://${formattedHost}:${config.port}/gateway/openai/v1`;
}

function isIgnorableCodexProcessLine(rawLine: string): boolean {
const line = normalizeCodexProcessLine(rawLine);
if (!line) {
Expand Down Expand Up @@ -669,6 +695,32 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
this.runPromise = services ? Effect.runPromiseWith(services) : Effect.runPromise;
}

private async resolveGatewayProviderConfig(model: string | undefined): Promise<
| {
readonly baseUrl: string;
readonly apiKey: string;
}
| undefined
> {
if (!resolveDeepSeekGatewayCodexModel(model)) {
return undefined;
}

try {
const config = (await this.runPromise(
Effect.gen(function* () {
return yield* ServerConfig;
}),
)) as ServerConfigShape;
return {
baseUrl: localGatewayBaseUrl(config),
apiKey: config.authToken ?? "peakcode-local-gateway",
};
} catch {
return undefined;
}
}

async startSession(input: CodexAppServerStartSessionInput): Promise<ProviderSession> {
const threadId = input.threadId;
const now = new Date().toISOString();
Expand Down Expand Up @@ -696,6 +748,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
const codexOptions = readCodexProviderOptions(input);
const codexBinaryPath = codexOptions.binaryPath ?? "codex";
const codexHomePath = codexOptions.homePath;
const gatewayProvider = await this.resolveGatewayProviderConfig(input.model);
this.assertSupportedCodexCliVersion({
binaryPath: codexBinaryPath,
cwd: resolvedCwd,
Expand All @@ -705,6 +758,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
cwd: resolvedCwd,
env: buildCodexProcessEnv({
...(codexHomePath ? { homePath: codexHomePath } : {}),
...(gatewayProvider ? { gatewayProvider } : {}),
}),
stdio: ["pipe", "pipe", "pipe"],
shell: process.platform === "win32",
Expand Down Expand Up @@ -1314,6 +1368,9 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
});
const codexBinaryPath = codexOptions.binaryPath ?? "codex";
const codexHomePath = codexOptions.homePath;
const gatewayProvider = await this.resolveGatewayProviderConfig(
input.modelSelection?.provider === "codex" ? input.modelSelection.model : undefined,
);
this.assertSupportedCodexCliVersion({
binaryPath: codexBinaryPath,
cwd: resolvedCwd,
Expand All @@ -1323,6 +1380,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
cwd: resolvedCwd,
env: buildCodexProcessEnv({
...(codexHomePath ? { homePath: codexHomePath } : {}),
...(gatewayProvider ? { gatewayProvider } : {}),
}),
stdio: ["pipe", "pipe", "pipe"],
shell: process.platform === "win32",
Expand Down
Loading
Loading