Skip to content
Merged
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"ink-gradient": "^4.0.0",
"openai": "^6.35.0",
"react": "^19.2.5",
"undici": "^7.25.0",
"zod": "^4.4.3"
},
"devDependencies": {
Expand Down
117 changes: 117 additions & 0 deletions src/common/openai-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import OpenAI from "openai";
import { Agent, fetch as undiciFetch } from "undici";
import { resolveCurrentSettings } from "../ui/App";

// Custom undici Agent with a 180-second keepAlive timeout. The default
// global fetch (undici) only keeps connections alive for 4 seconds, which
// is too short for a CLI where the user may spend 10–30 seconds reading
// output between prompts. By passing a dedicated Agent to undiciFetch we
// keep connections reusable for three minutes after the last request.
const keepAliveAgent = new Agent({ keepAliveTimeout: 180_000 });

// Module-level cache for the OpenAI client instance. The client itself is
// a stateless fetch wrapper, so it is safe to share across calls as long as
// the apiKey + baseURL stay the same. Model, thinking-mode and other
// settings are always read fresh from the project / user config files.
let cachedOpenAI: OpenAI | null = null;
let cachedOpenAIKey = "";

export function createOpenAIClient(projectRoot: string = process.cwd()): {
client: OpenAI | null;
model: string;
baseURL: string;
thinkingEnabled: boolean;
reasoningEffort: "high" | "max";
debugLogEnabled: boolean;
notify?: string;
webSearchTool?: string;
env: Record<string, string>;
machineId?: string;
} {
const settings = resolveCurrentSettings(projectRoot);
if (!settings.apiKey) {
return {
client: null,
model: settings.model,
baseURL: settings.baseURL,
thinkingEnabled: settings.thinkingEnabled,
reasoningEffort: settings.reasoningEffort,
debugLogEnabled: settings.debugLogEnabled,
notify: settings.notify,
webSearchTool: settings.webSearchTool,
env: settings.env,
machineId: getMachineId(),
};
}

const cacheKey = `${settings.apiKey}::${settings.baseURL}`;
if (cachedOpenAI && cachedOpenAIKey === cacheKey) {
return {
client: cachedOpenAI,
model: settings.model,
baseURL: settings.baseURL,
thinkingEnabled: settings.thinkingEnabled,
reasoningEffort: settings.reasoningEffort,
debugLogEnabled: settings.debugLogEnabled,
notify: settings.notify,
webSearchTool: settings.webSearchTool,
env: settings.env,
machineId: getMachineId(),
};
}

cachedOpenAI = new OpenAI({
apiKey: settings.apiKey,
baseURL: settings.baseURL || undefined,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fetch: (url: any, init: any) => undiciFetch(url, { ...init, dispatcher: keepAliveAgent }),
});
cachedOpenAIKey = cacheKey;

// Fire-and-forget warmup: pre-establish TCP+TLS connection to the API
// server while the user is composing their first prompt. Bounded by a
// short timeout so a slow / unreachable API never blocks process exit.
void (async () => {
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(), 3000);
try {
await cachedOpenAI.models.list({ signal: ac.signal }).catch(() => {});
} finally {
clearTimeout(timer);
}
})();

return {
client: cachedOpenAI,
model: settings.model,
baseURL: settings.baseURL,
thinkingEnabled: settings.thinkingEnabled,
reasoningEffort: settings.reasoningEffort,
debugLogEnabled: settings.debugLogEnabled,
notify: settings.notify,
webSearchTool: settings.webSearchTool,
env: settings.env,
machineId: getMachineId(),
};
}

function getMachineId(): string | undefined {
try {
const idPath = path.join(os.homedir(), ".deepcode", "machine-id");
if (fs.existsSync(idPath)) {
const raw = fs.readFileSync(idPath, "utf8").trim();
if (raw) {
return raw;
}
}
const generated = `${os.hostname()}-${Math.random().toString(36).slice(2)}-${Date.now()}`;
fs.mkdirSync(path.dirname(idPath), { recursive: true });
fs.writeFileSync(idPath, generated, "utf8");
return generated;
} catch {
return undefined;
}
}
73 changes: 9 additions & 64 deletions src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import chalk from "chalk";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import OpenAI from "openai";
import { createOpenAIClient } from "../common/openai-client";
import {
type LlmStreamProgress,
type MessageMeta,
Expand Down Expand Up @@ -166,6 +166,13 @@ export function App({ projectRoot, initialPrompt, onRestart }: AppProps): React.
void refreshSkills();
}, [refreshSessionsList, refreshSkills]);

// Eagerly create the OpenAI client on mount so the TCP+TLS connection
// warmup (fire-and-forget inside createOpenAIClient) starts before the
// user sends their first prompt.
useEffect(() => {
createOpenAIClient(projectRoot);
}, [projectRoot]);

useLayoutEffect(() => {
const settings = resolveCurrentSettings(projectRoot);
void sessionManager.initMcpServers(settings.mcpServers);
Expand Down Expand Up @@ -838,69 +845,7 @@ export function resolveCurrentSettings(projectRoot: string = process.cwd()): Res
);
}

export function createOpenAIClient(projectRoot: string = process.cwd()): {
client: OpenAI | null;
model: string;
baseURL: string;
thinkingEnabled: boolean;
reasoningEffort: "high" | "max";
debugLogEnabled: boolean;
notify?: string;
webSearchTool?: string;
env: Record<string, string>;
machineId?: string;
} {
const settings = resolveCurrentSettings(projectRoot);
if (!settings.apiKey) {
return {
client: null,
model: settings.model,
baseURL: settings.baseURL,
thinkingEnabled: settings.thinkingEnabled,
reasoningEffort: settings.reasoningEffort,
debugLogEnabled: settings.debugLogEnabled,
notify: settings.notify,
webSearchTool: settings.webSearchTool,
env: settings.env,
machineId: getMachineId(),
};
}

const client = new OpenAI({
apiKey: settings.apiKey,
baseURL: settings.baseURL || undefined,
});
return {
client,
model: settings.model,
baseURL: settings.baseURL,
thinkingEnabled: settings.thinkingEnabled,
reasoningEffort: settings.reasoningEffort,
debugLogEnabled: settings.debugLogEnabled,
notify: settings.notify,
webSearchTool: settings.webSearchTool,
env: settings.env,
machineId: getMachineId(),
};
}

function getMachineId(): string | undefined {
try {
const idPath = path.join(os.homedir(), ".deepcode", "machine-id");
if (fs.existsSync(idPath)) {
const raw = fs.readFileSync(idPath, "utf8").trim();
if (raw) {
return raw;
}
}
const generated = `${os.hostname()}-${Math.random().toString(36).slice(2)}-${Date.now()}`;
fs.mkdirSync(path.dirname(idPath), { recursive: true });
fs.writeFileSync(idPath, generated, "utf8");
return generated;
} catch {
return undefined;
}
}
export { createOpenAIClient } from "../common/openai-client";

function getUserSettingsPath(): string {
return path.join(os.homedir(), ".deepcode", "settings.json");
Expand Down
2 changes: 1 addition & 1 deletion src/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ export {
writeProjectSettings,
writeModelConfigSelection,
resolveCurrentSettings,
createOpenAIClient,
buildPromptDraftFromSessionMessage,
} from "./App";
export { createOpenAIClient } from "../common/openai-client";
export { default as AppContainer } from "./AppContainer";
export { AskUserQuestionPrompt } from "./AskUserQuestionPrompt";
export { MessageView } from "./components";
Expand Down
Loading