fix(proxy): preserve legacy state and exact ports

This commit is contained in:
Wooseong Kim committed 2026-04-20 14:03:12 +09:00
1 parent 630a493cca
commit 24c24847f7
5 files changed
+309 -41

No files matched your search

+8
View File
@@ -25,3 +25,11 @@ export function getOpenAICompatProxySessionPath(profileName: string): string {
`${getOpenAICompatProxyProfileKey(profileName)}.session.json`
);
}
export function getLegacyOpenAICompatProxyPidPath(): string {
return path.join(getOpenAICompatProxyDir(), 'daemon.pid');
}
export function getLegacyOpenAICompatProxySessionPath(): string {
return path.join(getOpenAICompatProxyDir(), 'session.json');
}
+38 -18
View File
@@ -2,6 +2,8 @@ import * as fs from 'fs';
import * as path from 'path';
import {
getOpenAICompatProxyDir,
getLegacyOpenAICompatProxyPidPath,
getLegacyOpenAICompatProxySessionPath,
getOpenAICompatProxyPidPath,
getOpenAICompatProxySessionPath,
} from './proxy-daemon-paths';
@@ -21,9 +23,9 @@ function ensureProxyDir(): void {
fs.mkdirSync(getOpenAICompatProxyDir(), { recursive: true });
}
export function getOpenAICompatProxyPid(profileName: string): number | null {
function readPid(pidPath: string): number | null {
try {
const raw = fs.readFileSync(getOpenAICompatProxyPidPath(profileName), 'utf8').trim();
const raw = fs.readFileSync(pidPath, 'utf8').trim();
const pid = Number.parseInt(raw, 10);
return Number.isInteger(pid) ? pid : null;
} catch {
@@ -31,6 +33,14 @@ export function getOpenAICompatProxyPid(profileName: string): number | null {
}
}
export function getOpenAICompatProxyPid(profileName: string): number | null {
return readPid(getOpenAICompatProxyPidPath(profileName));
}
export function getLegacyOpenAICompatProxyPid(): number | null {
return readPid(getLegacyOpenAICompatProxyPidPath());
}
export function writeOpenAICompatProxyPid(profileName: string, pid: number): void {
ensureProxyDir();
fs.writeFileSync(getOpenAICompatProxyPidPath(profileName), String(pid), 'utf8');
@@ -44,16 +54,30 @@ export function removeOpenAICompatProxyPid(profileName: string): void {
}
}
export function readOpenAICompatProxySession(profileName: string): OpenAICompatProxySession | null {
export function removeLegacyOpenAICompatProxyPid(): void {
try {
return JSON.parse(
fs.readFileSync(getOpenAICompatProxySessionPath(profileName), 'utf8')
) as OpenAICompatProxySession;
fs.unlinkSync(getLegacyOpenAICompatProxyPidPath());
} catch {
// Best-effort cleanup.
}
}
function readSession(sessionPath: string): OpenAICompatProxySession | null {
try {
return JSON.parse(fs.readFileSync(sessionPath, 'utf8')) as OpenAICompatProxySession;
} catch {
return null;
}
}
export function readOpenAICompatProxySession(profileName: string): OpenAICompatProxySession | null {
return readSession(getOpenAICompatProxySessionPath(profileName));
}
export function readLegacyOpenAICompatProxySession(): OpenAICompatProxySession | null {
return readSession(getLegacyOpenAICompatProxySessionPath());
}
export function writeOpenAICompatProxySession(session: OpenAICompatProxySession): void {
ensureProxyDir();
fs.writeFileSync(
@@ -71,6 +95,14 @@ export function removeOpenAICompatProxySession(profileName: string): void {
}
}
export function removeLegacyOpenAICompatProxySession(): void {
try {
fs.unlinkSync(getLegacyOpenAICompatProxySessionPath());
} catch {
// Best-effort cleanup.
}
}
export function listOpenAICompatProxyProfileNames(): string[] {
try {
const entries = fs.readdirSync(getOpenAICompatProxyDir(), { withFileTypes: true });
@@ -87,18 +119,6 @@ export function listOpenAICompatProxyProfileNames(): string[] {
}
}
export function cleanupLegacyOpenAICompatProxyState(): void {
ensureProxyDir();
const legacyNames = ['daemon.pid', 'session.json'];
for (const legacyName of legacyNames) {
try {
fs.unlinkSync(path.join(getOpenAICompatProxyDir(), legacyName));
} catch {
// Best-effort cleanup.
}
}
}
export function resolveOpenAICompatProxyEntrypointCandidates(): string[] {
const jsEntry = path.join(__dirname, 'proxy-daemon-entry.js');
const tsEntry = path.join(__dirname, 'proxy-daemon-entry.ts');
+83 -20
View File
@@ -12,10 +12,13 @@ import {
getOpenAICompatProxyDir,
} from './proxy-daemon-paths';
import {
getLegacyOpenAICompatProxyPid,
getOpenAICompatProxyPid,
listOpenAICompatProxyProfileNames,
readLegacyOpenAICompatProxySession,
readOpenAICompatProxySession,
cleanupLegacyOpenAICompatProxyState,
removeLegacyOpenAICompatProxyPid,
removeLegacyOpenAICompatProxySession,
removeOpenAICompatProxyPid,
removeOpenAICompatProxySession,
resolveOpenAICompatProxyEntrypointCandidates,
@@ -23,7 +26,7 @@ import {
writeOpenAICompatProxyPid,
writeOpenAICompatProxySession,
} from './proxy-daemon-state';
import { resolveOpenAICompatProxyPreferredPort } from './proxy-port-resolver';
import { resolveOpenAICompatProxyPortPreference } from './proxy-port-resolver';
export interface OpenAICompatProxyStatus extends Partial<OpenAICompatProxySession> {
running: boolean;
@@ -111,6 +114,12 @@ async function findOpenAICompatProxyPortNear(preferredPort: number): Promise<num
return findOpenAICompatProxyPort();
}
interface OpenAICompatProxyStateRecord {
pid: number | null;
session: OpenAICompatProxySession | null;
source: 'profile' | 'legacy';
}
async function resolveDaemonEntrypoint(): Promise<string | null> {
for (const candidate of resolveOpenAICompatProxyEntrypointCandidates()) {
try {
@@ -157,20 +166,51 @@ export async function isOpenAICompatProxyRunning(port: number): Promise<boolean>
async function getOpenAICompatProxyStatusForProfile(
profileName: string
): Promise<OpenAICompatProxyStatus> {
const session = readOpenAICompatProxySession(profileName);
const port = session?.port ?? OPENAI_COMPAT_PROXY_DEFAULT_PORT;
const state = getOpenAICompatProxyStateForProfile(profileName);
const session = state.session;
if (!session) {
return { running: false };
}
const port = session.port;
const running = await isOpenAICompatProxyRunning(port);
return {
running,
pid: running ? getOpenAICompatProxyPid(profileName) || undefined : undefined,
pid: running ? state.pid || undefined : undefined,
...session,
};
}
function getOpenAICompatProxyStateForProfile(profileName: string): OpenAICompatProxyStateRecord {
const session = readOpenAICompatProxySession(profileName);
if (session) {
return {
pid: getOpenAICompatProxyPid(profileName),
session,
source: 'profile',
};
}
const legacySession = readLegacyOpenAICompatProxySession();
if (legacySession?.profileName === profileName) {
return {
pid: getLegacyOpenAICompatProxyPid(),
session: legacySession,
source: 'legacy',
};
}
return { pid: null, session: null, source: 'profile' };
}
export async function listOpenAICompatProxyStatuses(): Promise<OpenAICompatProxyStatus[]> {
const profileNames = listOpenAICompatProxyProfileNames();
const profileNames = new Set(listOpenAICompatProxyProfileNames());
const legacySession = readLegacyOpenAICompatProxySession();
if (legacySession?.profileName) {
profileNames.add(legacySession.profileName);
}
const statuses = await Promise.all(
profileNames.map((profileName) => getOpenAICompatProxyStatusForProfile(profileName))
[...profileNames].map((profileName) => getOpenAICompatProxyStatusForProfile(profileName))
);
return statuses.filter((status) => status.profileName);
}
@@ -197,9 +237,10 @@ export async function getOpenAICompatProxyStatus(
async function stopOpenAICompatProxyUnlocked(
profileName: string
): Promise<{ success: boolean; error?: string }> {
const pid = getOpenAICompatProxyPid(profileName);
const state = getOpenAICompatProxyStateForProfile(profileName);
const pid = state.pid;
if (!pid) {
removeOpenAICompatProxySession(profileName);
removeOpenAICompatProxyState(state, profileName);
return { success: true };
}
@@ -211,8 +252,7 @@ async function stopOpenAICompatProxyUnlocked(
);
if (ownership === 'not-owned') {
removeOpenAICompatProxyPid(profileName);
removeOpenAICompatProxySession(profileName);
removeOpenAICompatProxyState(state, profileName);
return { success: true };
}
@@ -224,8 +264,7 @@ async function stopOpenAICompatProxyUnlocked(
}
if (ownership === 'not-running') {
removeOpenAICompatProxyPid(profileName);
removeOpenAICompatProxySession(profileName);
removeOpenAICompatProxyState(state, profileName);
return { success: true };
}
@@ -256,16 +295,28 @@ async function stopOpenAICompatProxyUnlocked(
}
}
removeOpenAICompatProxyState(state, profileName);
return { success: true };
}
function removeOpenAICompatProxyState(
state: OpenAICompatProxyStateRecord,
profileName: string
): void {
if (state.source === 'legacy') {
removeLegacyOpenAICompatProxyPid();
removeLegacyOpenAICompatProxySession();
return;
}
removeOpenAICompatProxyPid(profileName);
removeOpenAICompatProxySession(profileName);
return { success: true };
}
export async function stopOpenAICompatProxy(
profileName?: string
): Promise<{ success: boolean; error?: string }> {
return withOpenAICompatProxyLock(async () => {
cleanupLegacyOpenAICompatProxyState();
if (profileName) {
return stopOpenAICompatProxyUnlocked(profileName);
}
@@ -289,17 +340,22 @@ export async function startOpenAICompatProxy(
options: { port?: number; host?: string; insecure?: boolean } = {}
): Promise<StartOpenAICompatProxyResult> {
return withOpenAICompatProxyLock(async () => {
cleanupLegacyOpenAICompatProxyState();
const status = await getOpenAICompatProxyStatus(profile.profileName);
const host = options.host?.trim() || status.host || '127.0.0.1';
const portPreference = resolveOpenAICompatProxyPortPreference(profile.profileName);
const explicitPort = typeof options.port === 'number' ? options.port : undefined;
const preferredPort =
typeof options.port === 'number'
? options.port
: status.port || resolveOpenAICompatProxyPreferredPort(profile.profileName);
explicitPort ??
(portPreference.source === 'profile'
? portPreference.port
: status.port || portPreference.port);
const requiresExactPort = explicitPort !== undefined || portPreference.source === 'profile';
const port =
status.running && status.port && (status.host || '127.0.0.1') === host
? status.port
: await findOpenAICompatProxyPortNear(preferredPort);
: requiresExactPort
? preferredPort
: await findOpenAICompatProxyPortNear(preferredPort);
if (port === 0) {
return {
success: false,
@@ -319,6 +375,13 @@ export async function startOpenAICompatProxy(
authToken: status.authToken,
};
}
if (requiresExactPort && (await isPortOccupied(port))) {
return {
success: false,
port,
error: `Requested proxy port ${port} is already in use`,
};
}
if (status.running) {
const stopped = await stopOpenAICompatProxyUnlocked(profile.profileName);
if (!stopped.success) {
+17 -3
View File
@@ -1,11 +1,25 @@
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
import { OPENAI_COMPAT_PROXY_DEFAULT_PORT } from './proxy-daemon-paths';
export function resolveOpenAICompatProxyPreferredPort(profileName: string): number {
export interface OpenAICompatProxyPortPreference {
port: number;
source: 'default' | 'profile';
}
export function resolveOpenAICompatProxyPortPreference(
profileName: string
): OpenAICompatProxyPortPreference {
const config = loadOrCreateUnifiedConfig();
const profilePort = config.proxy?.profile_ports?.[profileName];
if (typeof profilePort === 'number') {
return profilePort;
return { port: profilePort, source: 'profile' };
}
return config.proxy?.port ?? OPENAI_COMPAT_PROXY_DEFAULT_PORT;
return {
port: config.proxy?.port ?? OPENAI_COMPAT_PROXY_DEFAULT_PORT,
source: 'default',
};
}
export function resolveOpenAICompatProxyPreferredPort(profileName: string): number {
return resolveOpenAICompatProxyPortPreference(profileName).port;
}