mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-11 14:22:06 +00:00
Merge pull request #1074 from kaitranntt/kai/feat/1073-adaptive-proxy-ports
feat(proxy): add adaptive local port selection for OpenAI-compatible profiles
This commit is contained in:
@@ -64,7 +64,7 @@ function showHelp(): number {
|
||||
console.log(' activate [profile] Print shell exports for the running proxy');
|
||||
console.log('');
|
||||
console.log('Options:');
|
||||
console.log(' --port <n> Override the local proxy port (default: 3456)');
|
||||
console.log(' --port <n> Pin an exact local proxy port for this launch');
|
||||
console.log(' --host <addr> Bind the proxy server to a specific host (default: 127.0.0.1)');
|
||||
console.log(' --shell <name> activate only: auto|bash|zsh|fish|powershell');
|
||||
console.log(' --fish activate only: shorthand for --shell fish');
|
||||
@@ -104,7 +104,19 @@ async function handleStart(args: string[]): Promise<number> {
|
||||
|
||||
const portValue = parseOptionValue(args, '--port');
|
||||
const host = parseOptionValue(args, '--host');
|
||||
const port = portValue ? Number.parseInt(portValue, 10) || 3456 : undefined;
|
||||
const parsedPort = portValue ? Number(portValue) : undefined;
|
||||
if (
|
||||
portValue &&
|
||||
(parsedPort === undefined ||
|
||||
!/^\d+$/.test(portValue) ||
|
||||
!Number.isInteger(parsedPort) ||
|
||||
parsedPort < 1 ||
|
||||
parsedPort > 65535)
|
||||
) {
|
||||
console.error(fail(`Invalid port: ${portValue}`));
|
||||
return 1;
|
||||
}
|
||||
const port = parsedPort;
|
||||
let profile;
|
||||
try {
|
||||
profile = resolveProfile(profileName);
|
||||
|
||||
@@ -1006,7 +1006,6 @@ export const DEFAULT_CLIPROXY_SERVER_CONFIG: CliproxyServerConfig = {
|
||||
};
|
||||
|
||||
export const DEFAULT_OPENAI_COMPAT_PROXY_CONFIG: OpenAICompatProxyConfig = {
|
||||
port: 3456,
|
||||
profile_ports: {},
|
||||
routing: {
|
||||
longContextThreshold: 60_000,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { loadSettings } from '../utils/config-manager';
|
||||
import { resolveOpenAICompatProfileConfig } from './profile-router';
|
||||
import { OPENAI_COMPAT_PROXY_DEFAULT_PORT } from './proxy-daemon-paths';
|
||||
import { startOpenAICompatProxyServer } from './server/proxy-server';
|
||||
|
||||
interface RuntimeOptions {
|
||||
@@ -12,7 +13,7 @@ interface RuntimeOptions {
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): RuntimeOptions {
|
||||
let port = 3456;
|
||||
let port = OPENAI_COMPAT_PROXY_DEFAULT_PORT;
|
||||
let host = '127.0.0.1';
|
||||
let profileName = '';
|
||||
let settingsPath = '';
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import * as path from 'path';
|
||||
import { getCcsDir } from '../utils/config-manager';
|
||||
|
||||
export const OPENAI_COMPAT_PROXY_DEFAULT_PORT = 3456;
|
||||
export const OPENAI_COMPAT_PROXY_LEGACY_DEFAULT_PORT = 3456;
|
||||
export const OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_START = 43_456;
|
||||
export const OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_END = 43_555;
|
||||
export const OPENAI_COMPAT_PROXY_DEFAULT_PORT = OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_START;
|
||||
export const OPENAI_COMPAT_PROXY_SERVICE_NAME = 'ccs-openai-compat-proxy';
|
||||
|
||||
export function getOpenAICompatProxyDir(): string {
|
||||
|
||||
+28
-25
@@ -6,7 +6,8 @@ import * as lockfile from 'proper-lockfile';
|
||||
import { verifyProcessOwnership } from '../cursor/daemon-process-ownership';
|
||||
import type { OpenAICompatProfileConfig } from './profile-router';
|
||||
import {
|
||||
OPENAI_COMPAT_PROXY_DEFAULT_PORT,
|
||||
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_END,
|
||||
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_START,
|
||||
OPENAI_COMPAT_PROXY_SERVICE_NAME,
|
||||
getOpenAICompatProxyDir,
|
||||
} from './proxy-daemon-paths';
|
||||
@@ -25,7 +26,10 @@ import {
|
||||
writeOpenAICompatProxyPid,
|
||||
writeOpenAICompatProxySession,
|
||||
} from './proxy-daemon-state';
|
||||
import { resolveOpenAICompatProxyPortPreference } from './proxy-port-resolver';
|
||||
import {
|
||||
listOpenAICompatProxyCandidatePorts as listFlexibleOpenAICompatProxyCandidatePorts,
|
||||
resolveOpenAICompatProxyPortPreference,
|
||||
} from './proxy-port-resolver';
|
||||
|
||||
export interface OpenAICompatProxyStatus extends Partial<OpenAICompatProxySession> {
|
||||
running: boolean;
|
||||
@@ -131,6 +135,7 @@ async function terminateDaemonProcess(pid?: number): Promise<void> {
|
||||
}
|
||||
|
||||
function listOpenAICompatProxyCandidatePorts(
|
||||
profileName: string,
|
||||
preferredPort: number,
|
||||
exact: boolean,
|
||||
excludedPorts: ReadonlySet<number> = new Set()
|
||||
@@ -139,22 +144,7 @@ function listOpenAICompatProxyCandidatePorts(
|
||||
return excludedPorts.has(preferredPort) ? [] : [preferredPort];
|
||||
}
|
||||
|
||||
const candidates = new Set<number>();
|
||||
if (!excludedPorts.has(preferredPort)) {
|
||||
candidates.add(preferredPort);
|
||||
}
|
||||
|
||||
for (
|
||||
let candidate = OPENAI_COMPAT_PROXY_DEFAULT_PORT;
|
||||
candidate <= OPENAI_COMPAT_PROXY_DEFAULT_PORT + 10;
|
||||
candidate += 1
|
||||
) {
|
||||
if (!excludedPorts.has(candidate)) {
|
||||
candidates.add(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return [...candidates];
|
||||
return listFlexibleOpenAICompatProxyCandidatePorts(profileName, preferredPort, excludedPorts);
|
||||
}
|
||||
|
||||
function isPortBindConflictMessage(message?: string): boolean {
|
||||
@@ -494,12 +484,24 @@ export async function startOpenAICompatProxy(
|
||||
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 =
|
||||
explicitPort ??
|
||||
(portPreference.source === 'profile'
|
||||
? portPreference.port
|
||||
: status.port || portPreference.port);
|
||||
const requiresExactPort = explicitPort !== undefined || portPreference.source === 'profile';
|
||||
if (
|
||||
status.running &&
|
||||
explicitPort === undefined &&
|
||||
portPreference.source !== 'profile' &&
|
||||
status.port &&
|
||||
(status.host || '127.0.0.1') === host
|
||||
) {
|
||||
return {
|
||||
success: true,
|
||||
alreadyRunning: true,
|
||||
pid: status.pid,
|
||||
port: status.port,
|
||||
authToken: status.authToken,
|
||||
};
|
||||
}
|
||||
|
||||
const preferredPort = explicitPort ?? portPreference.port;
|
||||
if (status.running && status.port === preferredPort && (status.host || '127.0.0.1') === host) {
|
||||
return {
|
||||
success: true,
|
||||
@@ -670,6 +672,7 @@ export async function startOpenAICompatProxy(
|
||||
const attemptedPorts = new Set<number>();
|
||||
let lastResult: OpenAICompatProxyLaunchResult | null = null;
|
||||
const candidates = listOpenAICompatProxyCandidatePorts(
|
||||
profile.profileName,
|
||||
preferredPort,
|
||||
requiresExactPort,
|
||||
attemptedPorts
|
||||
@@ -678,7 +681,7 @@ export async function startOpenAICompatProxy(
|
||||
return {
|
||||
success: false,
|
||||
port: preferredPort,
|
||||
error: `No free proxy port found in range ${OPENAI_COMPAT_PROXY_DEFAULT_PORT}-${OPENAI_COMPAT_PROXY_DEFAULT_PORT + 10}`,
|
||||
error: `No free proxy port found in adaptive range ${OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_START}-${OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_END}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -707,7 +710,7 @@ export async function startOpenAICompatProxy(
|
||||
port: preferredPort,
|
||||
error: requiresExactPort
|
||||
? `Requested proxy port ${preferredPort} is already in use`
|
||||
: 'No free proxy port found in the proxy port range',
|
||||
: 'No free proxy port found in the adaptive proxy port range',
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,55 @@
|
||||
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
|
||||
import { OPENAI_COMPAT_PROXY_DEFAULT_PORT } from './proxy-daemon-paths';
|
||||
import {
|
||||
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_END,
|
||||
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_START,
|
||||
OPENAI_COMPAT_PROXY_LEGACY_DEFAULT_PORT,
|
||||
} from './proxy-daemon-paths';
|
||||
|
||||
export interface OpenAICompatProxyPortPreference {
|
||||
port: number;
|
||||
source: 'default' | 'profile';
|
||||
source: 'adaptive' | 'profile' | 'shared';
|
||||
}
|
||||
|
||||
const ADAPTIVE_PORT_RANGE_SIZE =
|
||||
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_END - OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_START + 1;
|
||||
|
||||
function hashProfileName(profileName: string): number {
|
||||
let hash = 0;
|
||||
for (const char of profileName.trim()) {
|
||||
hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
export function resolveOpenAICompatProxyAdaptivePort(profileName: string): number {
|
||||
return (
|
||||
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_START +
|
||||
(hashProfileName(profileName) % ADAPTIVE_PORT_RANGE_SIZE)
|
||||
);
|
||||
}
|
||||
|
||||
export function listOpenAICompatProxyCandidatePorts(
|
||||
profileName: string,
|
||||
preferredPort: number,
|
||||
excludedPorts: ReadonlySet<number> = new Set()
|
||||
): number[] {
|
||||
const candidates = new Set<number>();
|
||||
if (!excludedPorts.has(preferredPort)) {
|
||||
candidates.add(preferredPort);
|
||||
}
|
||||
|
||||
const adaptiveStart = resolveOpenAICompatProxyAdaptivePort(profileName);
|
||||
for (let offset = 0; offset < ADAPTIVE_PORT_RANGE_SIZE; offset += 1) {
|
||||
const candidate =
|
||||
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_START +
|
||||
((adaptiveStart - OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_START + offset) %
|
||||
ADAPTIVE_PORT_RANGE_SIZE);
|
||||
if (!excludedPorts.has(candidate)) {
|
||||
candidates.add(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return [...candidates];
|
||||
}
|
||||
|
||||
export function resolveOpenAICompatProxyPortPreference(
|
||||
@@ -14,9 +60,19 @@ export function resolveOpenAICompatProxyPortPreference(
|
||||
if (typeof profilePort === 'number') {
|
||||
return { port: profilePort, source: 'profile' };
|
||||
}
|
||||
const sharedPort = config.proxy?.port;
|
||||
if (typeof sharedPort === 'number') {
|
||||
if (sharedPort === OPENAI_COMPAT_PROXY_LEGACY_DEFAULT_PORT) {
|
||||
return {
|
||||
port: resolveOpenAICompatProxyAdaptivePort(profileName),
|
||||
source: 'adaptive',
|
||||
};
|
||||
}
|
||||
return { port: sharedPort, source: 'shared' };
|
||||
}
|
||||
return {
|
||||
port: config.proxy?.port ?? OPENAI_COMPAT_PROXY_DEFAULT_PORT,
|
||||
source: 'default',
|
||||
port: resolveOpenAICompatProxyAdaptivePort(profileName),
|
||||
source: 'adaptive',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user