mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 20:14:34 +00:00
fix(proxy): preserve legacy state and exact ports
This commit is contained in:
1 parent
630a493cca
commit
24c24847f7
5 files changed
+309
-41
No files matched your search
@@ -25,3 +25,11 @@ export function getOpenAICompatProxySessionPath(profileName: string): string {
|
|||||||
`${getOpenAICompatProxyProfileKey(profileName)}.session.json`
|
`${getOpenAICompatProxyProfileKey(profileName)}.session.json`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getLegacyOpenAICompatProxyPidPath(): string {
|
||||||
|
return path.join(getOpenAICompatProxyDir(), 'daemon.pid');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLegacyOpenAICompatProxySessionPath(): string {
|
||||||
|
return path.join(getOpenAICompatProxyDir(), 'session.json');
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import {
|
import {
|
||||||
getOpenAICompatProxyDir,
|
getOpenAICompatProxyDir,
|
||||||
|
getLegacyOpenAICompatProxyPidPath,
|
||||||
|
getLegacyOpenAICompatProxySessionPath,
|
||||||
getOpenAICompatProxyPidPath,
|
getOpenAICompatProxyPidPath,
|
||||||
getOpenAICompatProxySessionPath,
|
getOpenAICompatProxySessionPath,
|
||||||
} from './proxy-daemon-paths';
|
} from './proxy-daemon-paths';
|
||||||
@@ -21,9 +23,9 @@ function ensureProxyDir(): void {
|
|||||||
fs.mkdirSync(getOpenAICompatProxyDir(), { recursive: true });
|
fs.mkdirSync(getOpenAICompatProxyDir(), { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOpenAICompatProxyPid(profileName: string): number | null {
|
function readPid(pidPath: string): number | null {
|
||||||
try {
|
try {
|
||||||
const raw = fs.readFileSync(getOpenAICompatProxyPidPath(profileName), 'utf8').trim();
|
const raw = fs.readFileSync(pidPath, 'utf8').trim();
|
||||||
const pid = Number.parseInt(raw, 10);
|
const pid = Number.parseInt(raw, 10);
|
||||||
return Number.isInteger(pid) ? pid : null;
|
return Number.isInteger(pid) ? pid : null;
|
||||||
} catch {
|
} 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 {
|
export function writeOpenAICompatProxyPid(profileName: string, pid: number): void {
|
||||||
ensureProxyDir();
|
ensureProxyDir();
|
||||||
fs.writeFileSync(getOpenAICompatProxyPidPath(profileName), String(pid), 'utf8');
|
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 {
|
try {
|
||||||
return JSON.parse(
|
fs.unlinkSync(getLegacyOpenAICompatProxyPidPath());
|
||||||
fs.readFileSync(getOpenAICompatProxySessionPath(profileName), 'utf8')
|
} catch {
|
||||||
) as OpenAICompatProxySession;
|
// Best-effort cleanup.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSession(sessionPath: string): OpenAICompatProxySession | null {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(sessionPath, 'utf8')) as OpenAICompatProxySession;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
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 {
|
export function writeOpenAICompatProxySession(session: OpenAICompatProxySession): void {
|
||||||
ensureProxyDir();
|
ensureProxyDir();
|
||||||
fs.writeFileSync(
|
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[] {
|
export function listOpenAICompatProxyProfileNames(): string[] {
|
||||||
try {
|
try {
|
||||||
const entries = fs.readdirSync(getOpenAICompatProxyDir(), { withFileTypes: true });
|
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[] {
|
export function resolveOpenAICompatProxyEntrypointCandidates(): string[] {
|
||||||
const jsEntry = path.join(__dirname, 'proxy-daemon-entry.js');
|
const jsEntry = path.join(__dirname, 'proxy-daemon-entry.js');
|
||||||
const tsEntry = path.join(__dirname, 'proxy-daemon-entry.ts');
|
const tsEntry = path.join(__dirname, 'proxy-daemon-entry.ts');
|
||||||
|
|||||||
+83
-20
@@ -12,10 +12,13 @@ import {
|
|||||||
getOpenAICompatProxyDir,
|
getOpenAICompatProxyDir,
|
||||||
} from './proxy-daemon-paths';
|
} from './proxy-daemon-paths';
|
||||||
import {
|
import {
|
||||||
|
getLegacyOpenAICompatProxyPid,
|
||||||
getOpenAICompatProxyPid,
|
getOpenAICompatProxyPid,
|
||||||
listOpenAICompatProxyProfileNames,
|
listOpenAICompatProxyProfileNames,
|
||||||
|
readLegacyOpenAICompatProxySession,
|
||||||
readOpenAICompatProxySession,
|
readOpenAICompatProxySession,
|
||||||
cleanupLegacyOpenAICompatProxyState,
|
removeLegacyOpenAICompatProxyPid,
|
||||||
|
removeLegacyOpenAICompatProxySession,
|
||||||
removeOpenAICompatProxyPid,
|
removeOpenAICompatProxyPid,
|
||||||
removeOpenAICompatProxySession,
|
removeOpenAICompatProxySession,
|
||||||
resolveOpenAICompatProxyEntrypointCandidates,
|
resolveOpenAICompatProxyEntrypointCandidates,
|
||||||
@@ -23,7 +26,7 @@ import {
|
|||||||
writeOpenAICompatProxyPid,
|
writeOpenAICompatProxyPid,
|
||||||
writeOpenAICompatProxySession,
|
writeOpenAICompatProxySession,
|
||||||
} from './proxy-daemon-state';
|
} from './proxy-daemon-state';
|
||||||
import { resolveOpenAICompatProxyPreferredPort } from './proxy-port-resolver';
|
import { resolveOpenAICompatProxyPortPreference } from './proxy-port-resolver';
|
||||||
|
|
||||||
export interface OpenAICompatProxyStatus extends Partial<OpenAICompatProxySession> {
|
export interface OpenAICompatProxyStatus extends Partial<OpenAICompatProxySession> {
|
||||||
running: boolean;
|
running: boolean;
|
||||||
@@ -111,6 +114,12 @@ async function findOpenAICompatProxyPortNear(preferredPort: number): Promise<num
|
|||||||
return findOpenAICompatProxyPort();
|
return findOpenAICompatProxyPort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OpenAICompatProxyStateRecord {
|
||||||
|
pid: number | null;
|
||||||
|
session: OpenAICompatProxySession | null;
|
||||||
|
source: 'profile' | 'legacy';
|
||||||
|
}
|
||||||
|
|
||||||
async function resolveDaemonEntrypoint(): Promise<string | null> {
|
async function resolveDaemonEntrypoint(): Promise<string | null> {
|
||||||
for (const candidate of resolveOpenAICompatProxyEntrypointCandidates()) {
|
for (const candidate of resolveOpenAICompatProxyEntrypointCandidates()) {
|
||||||
try {
|
try {
|
||||||
@@ -157,20 +166,51 @@ export async function isOpenAICompatProxyRunning(port: number): Promise<boolean>
|
|||||||
async function getOpenAICompatProxyStatusForProfile(
|
async function getOpenAICompatProxyStatusForProfile(
|
||||||
profileName: string
|
profileName: string
|
||||||
): Promise<OpenAICompatProxyStatus> {
|
): Promise<OpenAICompatProxyStatus> {
|
||||||
const session = readOpenAICompatProxySession(profileName);
|
const state = getOpenAICompatProxyStateForProfile(profileName);
|
||||||
const port = session?.port ?? OPENAI_COMPAT_PROXY_DEFAULT_PORT;
|
const session = state.session;
|
||||||
|
if (!session) {
|
||||||
|
return { running: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const port = session.port;
|
||||||
const running = await isOpenAICompatProxyRunning(port);
|
const running = await isOpenAICompatProxyRunning(port);
|
||||||
return {
|
return {
|
||||||
running,
|
running,
|
||||||
pid: running ? getOpenAICompatProxyPid(profileName) || undefined : undefined,
|
pid: running ? state.pid || undefined : undefined,
|
||||||
...session,
|
...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[]> {
|
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(
|
const statuses = await Promise.all(
|
||||||
profileNames.map((profileName) => getOpenAICompatProxyStatusForProfile(profileName))
|
[...profileNames].map((profileName) => getOpenAICompatProxyStatusForProfile(profileName))
|
||||||
);
|
);
|
||||||
return statuses.filter((status) => status.profileName);
|
return statuses.filter((status) => status.profileName);
|
||||||
}
|
}
|
||||||
@@ -197,9 +237,10 @@ export async function getOpenAICompatProxyStatus(
|
|||||||
async function stopOpenAICompatProxyUnlocked(
|
async function stopOpenAICompatProxyUnlocked(
|
||||||
profileName: string
|
profileName: string
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
const pid = getOpenAICompatProxyPid(profileName);
|
const state = getOpenAICompatProxyStateForProfile(profileName);
|
||||||
|
const pid = state.pid;
|
||||||
if (!pid) {
|
if (!pid) {
|
||||||
removeOpenAICompatProxySession(profileName);
|
removeOpenAICompatProxyState(state, profileName);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,8 +252,7 @@ async function stopOpenAICompatProxyUnlocked(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (ownership === 'not-owned') {
|
if (ownership === 'not-owned') {
|
||||||
removeOpenAICompatProxyPid(profileName);
|
removeOpenAICompatProxyState(state, profileName);
|
||||||
removeOpenAICompatProxySession(profileName);
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,8 +264,7 @@ async function stopOpenAICompatProxyUnlocked(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ownership === 'not-running') {
|
if (ownership === 'not-running') {
|
||||||
removeOpenAICompatProxyPid(profileName);
|
removeOpenAICompatProxyState(state, profileName);
|
||||||
removeOpenAICompatProxySession(profileName);
|
|
||||||
return { success: true };
|
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);
|
removeOpenAICompatProxyPid(profileName);
|
||||||
removeOpenAICompatProxySession(profileName);
|
removeOpenAICompatProxySession(profileName);
|
||||||
return { success: true };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function stopOpenAICompatProxy(
|
export async function stopOpenAICompatProxy(
|
||||||
profileName?: string
|
profileName?: string
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
return withOpenAICompatProxyLock(async () => {
|
return withOpenAICompatProxyLock(async () => {
|
||||||
cleanupLegacyOpenAICompatProxyState();
|
|
||||||
if (profileName) {
|
if (profileName) {
|
||||||
return stopOpenAICompatProxyUnlocked(profileName);
|
return stopOpenAICompatProxyUnlocked(profileName);
|
||||||
}
|
}
|
||||||
@@ -289,17 +340,22 @@ export async function startOpenAICompatProxy(
|
|||||||
options: { port?: number; host?: string; insecure?: boolean } = {}
|
options: { port?: number; host?: string; insecure?: boolean } = {}
|
||||||
): Promise<StartOpenAICompatProxyResult> {
|
): Promise<StartOpenAICompatProxyResult> {
|
||||||
return withOpenAICompatProxyLock(async () => {
|
return withOpenAICompatProxyLock(async () => {
|
||||||
cleanupLegacyOpenAICompatProxyState();
|
|
||||||
const status = await getOpenAICompatProxyStatus(profile.profileName);
|
const status = await getOpenAICompatProxyStatus(profile.profileName);
|
||||||
const host = options.host?.trim() || status.host || '127.0.0.1';
|
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 =
|
const preferredPort =
|
||||||
typeof options.port === 'number'
|
explicitPort ??
|
||||||
? options.port
|
(portPreference.source === 'profile'
|
||||||
: status.port || resolveOpenAICompatProxyPreferredPort(profile.profileName);
|
? portPreference.port
|
||||||
|
: status.port || portPreference.port);
|
||||||
|
const requiresExactPort = explicitPort !== undefined || portPreference.source === 'profile';
|
||||||
const port =
|
const port =
|
||||||
status.running && status.port && (status.host || '127.0.0.1') === host
|
status.running && status.port && (status.host || '127.0.0.1') === host
|
||||||
? status.port
|
? status.port
|
||||||
: await findOpenAICompatProxyPortNear(preferredPort);
|
: requiresExactPort
|
||||||
|
? preferredPort
|
||||||
|
: await findOpenAICompatProxyPortNear(preferredPort);
|
||||||
if (port === 0) {
|
if (port === 0) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -319,6 +375,13 @@ export async function startOpenAICompatProxy(
|
|||||||
authToken: status.authToken,
|
authToken: status.authToken,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (requiresExactPort && (await isPortOccupied(port))) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
port,
|
||||||
|
error: `Requested proxy port ${port} is already in use`,
|
||||||
|
};
|
||||||
|
}
|
||||||
if (status.running) {
|
if (status.running) {
|
||||||
const stopped = await stopOpenAICompatProxyUnlocked(profile.profileName);
|
const stopped = await stopOpenAICompatProxyUnlocked(profile.profileName);
|
||||||
if (!stopped.success) {
|
if (!stopped.success) {
|
||||||
|
|||||||
@@ -1,11 +1,25 @@
|
|||||||
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
|
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
|
||||||
import { OPENAI_COMPAT_PROXY_DEFAULT_PORT } from './proxy-daemon-paths';
|
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 config = loadOrCreateUnifiedConfig();
|
||||||
const profilePort = config.proxy?.profile_ports?.[profileName];
|
const profilePort = config.proxy?.profile_ports?.[profileName];
|
||||||
if (typeof profilePort === 'number') {
|
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;
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,11 @@ import {
|
|||||||
stopOpenAICompatProxy,
|
stopOpenAICompatProxy,
|
||||||
} from '../../../src/proxy/proxy-daemon';
|
} from '../../../src/proxy/proxy-daemon';
|
||||||
import { resolveOpenAICompatProfileConfig } from '../../../src/proxy/profile-router';
|
import { resolveOpenAICompatProfileConfig } from '../../../src/proxy/profile-router';
|
||||||
|
import {
|
||||||
|
getLegacyOpenAICompatProxyPidPath,
|
||||||
|
getLegacyOpenAICompatProxySessionPath,
|
||||||
|
} from '../../../src/proxy/proxy-daemon-paths';
|
||||||
|
import { mutateUnifiedConfig } from '../../../src/config/unified-config-loader';
|
||||||
|
|
||||||
let originalCcsHome: string | undefined;
|
let originalCcsHome: string | undefined;
|
||||||
let tempDir: string;
|
let tempDir: string;
|
||||||
@@ -140,4 +145,162 @@ describe('openai proxy daemon lifecycle', () => {
|
|||||||
const secondHealth = await fetch(`http://127.0.0.1:${secondPort}/health`);
|
const secondHealth = await fetch(`http://127.0.0.1:${secondPort}/health`);
|
||||||
expect(secondHealth.status).toBe(200);
|
expect(secondHealth.status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps a legacy singleton daemon visible across upgrade', async () => {
|
||||||
|
const port = await getPort();
|
||||||
|
const settingsPath = path.join(tempDir, 'legacy.settings.json');
|
||||||
|
fs.writeFileSync(
|
||||||
|
settingsPath,
|
||||||
|
JSON.stringify({
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:11434',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'ollama-legacy',
|
||||||
|
ANTHROPIC_MODEL: 'qwen3-coder',
|
||||||
|
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const profile = resolveOpenAICompatProfileConfig('legacy', settingsPath, {
|
||||||
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:11434',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'ollama-legacy',
|
||||||
|
ANTHROPIC_MODEL: 'qwen3-coder',
|
||||||
|
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||||
|
});
|
||||||
|
if (!profile) {
|
||||||
|
throw new Error('Expected a legacy OpenAI-compatible profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
const started = await startOpenAICompatProxy(profile, { port });
|
||||||
|
expect(started.success).toBe(true);
|
||||||
|
expect(started.pid).toBeDefined();
|
||||||
|
|
||||||
|
const proxyDir = path.dirname(getLegacyOpenAICompatProxyPidPath());
|
||||||
|
fs.writeFileSync(getLegacyOpenAICompatProxyPidPath(), String(started.pid), 'utf8');
|
||||||
|
fs.writeFileSync(
|
||||||
|
getLegacyOpenAICompatProxySessionPath(),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
profileName: profile.profileName,
|
||||||
|
settingsPath: profile.settingsPath,
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port,
|
||||||
|
baseUrl: profile.baseUrl,
|
||||||
|
authToken: started.authToken,
|
||||||
|
model: profile.model,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
) + '\n',
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
fs.rmSync(path.join(proxyDir, 'legacy.daemon.pid'), { force: true });
|
||||||
|
fs.rmSync(path.join(proxyDir, 'legacy.session.json'), { force: true });
|
||||||
|
|
||||||
|
const status = await getOpenAICompatProxyStatus('legacy');
|
||||||
|
expect(status.running).toBe(true);
|
||||||
|
expect(status.port).toBe(port);
|
||||||
|
|
||||||
|
const restarted = await startOpenAICompatProxy(profile);
|
||||||
|
expect(restarted.success).toBe(true);
|
||||||
|
expect(restarted.alreadyRunning).toBe(true);
|
||||||
|
expect(restarted.port).toBe(port);
|
||||||
|
|
||||||
|
const stopped = await stopOpenAICompatProxy('legacy');
|
||||||
|
expect(stopped.success).toBe(true);
|
||||||
|
expect(fs.existsSync(getLegacyOpenAICompatProxyPidPath())).toBe(false);
|
||||||
|
expect(fs.existsSync(getLegacyOpenAICompatProxySessionPath())).toBe(false);
|
||||||
|
}, 35000);
|
||||||
|
|
||||||
|
it('fails when an explicit port is already occupied', async () => {
|
||||||
|
const occupiedPort = await getPort();
|
||||||
|
const server = Bun.serve({
|
||||||
|
port: occupiedPort,
|
||||||
|
hostname: '127.0.0.1',
|
||||||
|
fetch: () => new Response('busy'),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const settingsPath = path.join(tempDir, 'occupied-explicit.settings.json');
|
||||||
|
fs.writeFileSync(
|
||||||
|
settingsPath,
|
||||||
|
JSON.stringify({
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:11434',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'ollama-explicit',
|
||||||
|
ANTHROPIC_MODEL: 'qwen3-coder',
|
||||||
|
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const profile = resolveOpenAICompatProfileConfig('explicit', settingsPath, {
|
||||||
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:11434',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'ollama-explicit',
|
||||||
|
ANTHROPIC_MODEL: 'qwen3-coder',
|
||||||
|
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||||
|
});
|
||||||
|
if (!profile) {
|
||||||
|
throw new Error('Expected an explicit-port OpenAI-compatible profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
const started = await startOpenAICompatProxy(profile, { port: occupiedPort });
|
||||||
|
expect(started.success).toBe(false);
|
||||||
|
expect(started.port).toBe(occupiedPort);
|
||||||
|
expect(started.error).toContain(`Requested proxy port ${occupiedPort} is already in use`);
|
||||||
|
} finally {
|
||||||
|
server.stop(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails when a configured profile port is already occupied', async () => {
|
||||||
|
const occupiedPort = await getPort();
|
||||||
|
const server = Bun.serve({
|
||||||
|
port: occupiedPort,
|
||||||
|
hostname: '127.0.0.1',
|
||||||
|
fetch: () => new Response('busy'),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
mutateUnifiedConfig((config) => {
|
||||||
|
config.proxy = {
|
||||||
|
...(config.proxy ?? {}),
|
||||||
|
profile_ports: { mapped: occupiedPort },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const settingsPath = path.join(tempDir, 'occupied-mapped.settings.json');
|
||||||
|
fs.writeFileSync(
|
||||||
|
settingsPath,
|
||||||
|
JSON.stringify({
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:11434',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'ollama-mapped',
|
||||||
|
ANTHROPIC_MODEL: 'qwen3-coder',
|
||||||
|
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const profile = resolveOpenAICompatProfileConfig('mapped', settingsPath, {
|
||||||
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:11434',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'ollama-mapped',
|
||||||
|
ANTHROPIC_MODEL: 'qwen3-coder',
|
||||||
|
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||||
|
});
|
||||||
|
if (!profile) {
|
||||||
|
throw new Error('Expected a mapped-port OpenAI-compatible profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
const started = await startOpenAICompatProxy(profile);
|
||||||
|
expect(started.success).toBe(false);
|
||||||
|
expect(started.port).toBe(occupiedPort);
|
||||||
|
expect(started.error).toContain(`Requested proxy port ${occupiedPort} is already in use`);
|
||||||
|
} finally {
|
||||||
|
server.stop(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user