mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-18 14:13:21 +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:
11 files changed
+429
-111
No files matched your search
@@ -92,6 +92,20 @@ src/
|
|||||||
│ ├── unified-config-loader.ts # Central config loader (546 lines)
|
│ ├── unified-config-loader.ts # Central config loader (546 lines)
|
||||||
│ └── migration-manager.ts # Config migration logic
|
│ └── migration-manager.ts # Config migration logic
|
||||||
│
|
│
|
||||||
|
├── proxy/ # OpenAI-compatible proxy runtime
|
||||||
|
│ ├── index.ts # Barrel export
|
||||||
|
│ ├── proxy-daemon-entry.ts # Daemon entrypoint
|
||||||
|
│ ├── proxy-daemon.ts # Lifecycle, health, and port binding
|
||||||
|
│ ├── proxy-port-resolver.ts # Adaptive per-profile port selection
|
||||||
|
│ ├── request-router.ts # Request-time profile/model routing
|
||||||
|
│ ├── profile-router.ts # Profile resolution helpers
|
||||||
|
│ ├── proxy-env.ts # Local runtime env construction
|
||||||
|
│ ├── routing-config.ts # Proxy routing config parsing
|
||||||
|
│ ├── upstream-url.ts # Upstream endpoint resolution
|
||||||
|
│ ├── proxy-daemon-state.ts # Persistent running-state metadata
|
||||||
|
│ ├── server/ # HTTP server and routes
|
||||||
|
│ └── transformers/ # Request and SSE translation
|
||||||
|
│
|
||||||
├── channels/ # Official Claude channel integration
|
├── channels/ # Official Claude channel integration
|
||||||
│ ├── official-channels-runtime.ts # Runtime gating, plugin specs, setup guidance
|
│ ├── official-channels-runtime.ts # Runtime gating, plugin specs, setup guidance
|
||||||
│ └── official-channels-store.ts # Claude channel token/env storage helpers
|
│ └── official-channels-store.ts # Claude channel token/env storage helpers
|
||||||
@@ -216,6 +230,7 @@ src/
|
|||||||
| Targets | `bin/`, `targets/` | Multi-CLI adapter pattern (Claude Code, Factory Droid, Codex CLI, extensible) |
|
| Targets | `bin/`, `targets/` | Multi-CLI adapter pattern (Claude Code, Factory Droid, Codex CLI, extensible) |
|
||||||
| Auth | `auth/`, `cliproxy/auth/` | Authentication across providers |
|
| Auth | `auth/`, `cliproxy/auth/` | Authentication across providers |
|
||||||
| Config | `config/`, `types/` | Configuration & type definitions |
|
| Config | `config/`, `types/` | Configuration & type definitions |
|
||||||
|
| OpenAI Proxy | `proxy/` | Adaptive local OpenAI-compatible proxy runtime, profile routing, and SSE transforms |
|
||||||
| Providers | `cliproxy/`, `copilot/`, `glmt/` | Provider integrations plus retained legacy transformer internals |
|
| Providers | `cliproxy/`, `copilot/`, `glmt/` | Provider integrations plus retained legacy transformer internals |
|
||||||
| Quota | `cliproxy/quota-*.ts`, `account-manager.ts` | Hybrid quota management (v7.14) |
|
| Quota | `cliproxy/quota-*.ts`, `account-manager.ts` | Hybrid quota management (v7.14) |
|
||||||
| Remote Proxy | `cliproxy/remote-*.ts`, `proxy-config-resolver.ts` | Remote CLIProxy support (v7.1) |
|
| Remote Proxy | `cliproxy/remote-*.ts`, `proxy-config-resolver.ts` | Remote CLIProxy support (v7.1) |
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ When to use CCS:
|
|||||||
|
|
||||||
When you launch a compatible settings profile with the Claude target, CCS now:
|
When you launch a compatible settings profile with the Claude target, CCS now:
|
||||||
|
|
||||||
1. Starts a local proxy on `127.0.0.1`
|
1. Starts a local proxy on `127.0.0.1` using the resolved local port for that profile
|
||||||
2. Accepts Anthropic `/v1/messages` traffic from Claude Code
|
2. Accepts Anthropic `/v1/messages` traffic from Claude Code
|
||||||
3. Translates requests into OpenAI chat-completions format
|
3. Translates requests into OpenAI chat-completions format
|
||||||
4. Forwards them to your configured upstream provider
|
4. Forwards them to your configured upstream provider
|
||||||
@@ -72,12 +72,24 @@ Useful variants:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
ccs proxy start hf --host 127.0.0.1
|
ccs proxy start hf --host 127.0.0.1
|
||||||
|
ccs proxy start hf --port 3460
|
||||||
ccs proxy activate hf
|
ccs proxy activate hf
|
||||||
ccs proxy activate --fish
|
ccs proxy activate --fish
|
||||||
ccs proxy status hf
|
ccs proxy status hf
|
||||||
ccs proxy stop hf
|
ccs proxy stop hf
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Port selection precedence is:
|
||||||
|
|
||||||
|
1. CLI `--port` for an exact one-off pin
|
||||||
|
2. `proxy.profile_ports[profile]` for an exact per-profile pin
|
||||||
|
3. `proxy.port` for a shared preferred starting port
|
||||||
|
4. adaptive per-profile fallback when nothing is pinned
|
||||||
|
|
||||||
|
Legacy shared `proxy.port: 3456` values are treated as unset so older configs
|
||||||
|
move onto the adaptive path instead of staying on the hot legacy default. If
|
||||||
|
you need an exact `3456` binding now, pin it via `--port` or `proxy.profile_ports`.
|
||||||
|
|
||||||
`ccs proxy activate` now prints the full local runtime contract:
|
`ccs proxy activate` now prints the full local runtime contract:
|
||||||
|
|
||||||
- `ANTHROPIC_BASE_URL`
|
- `ANTHROPIC_BASE_URL`
|
||||||
@@ -98,14 +110,16 @@ runtime as a singleton.
|
|||||||
running
|
running
|
||||||
- When multiple proxies are running, pass the profile explicitly to
|
- When multiple proxies are running, pass the profile explicitly to
|
||||||
`activate`, `status`, or `stop`
|
`activate`, `status`, or `stop`
|
||||||
|
- `status` and `activate` always reflect the actual running port instead of an
|
||||||
|
assumed default
|
||||||
|
|
||||||
If you want deterministic ports, configure them in `~/.ccs/config.yaml`:
|
If you want to pin or guide ports explicitly, configure them in `~/.ccs/config.yaml`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
proxy:
|
proxy:
|
||||||
port: 3456
|
port: 45000
|
||||||
profile_ports:
|
profile_ports:
|
||||||
hf: 3456
|
hf: 3460
|
||||||
openai: 3461
|
openai: 3461
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -301,10 +315,12 @@ That flag is respected by both:
|
|||||||
- Add `CCS_OPENAI_PROXY_INSECURE=1` to the profile settings
|
- Add `CCS_OPENAI_PROXY_INSECURE=1` to the profile settings
|
||||||
- Restart the proxy after changing the setting
|
- Restart the proxy after changing the setting
|
||||||
|
|
||||||
### Port conflict on `3456`
|
### Need to pin or verify the local port
|
||||||
|
|
||||||
- Start with a fixed port: `ccs proxy start hf --port 3457`
|
- Check the active binding with `ccs proxy status hf`
|
||||||
- Re-run `ccs proxy activate` after changing the port
|
- Pin a one-off port with `ccs proxy start hf --port 3460`
|
||||||
|
- Reserve a stable profile port with `proxy.profile_ports`
|
||||||
|
- Re-run `ccs proxy activate hf` after changing the port
|
||||||
|
|
||||||
### Provider returns `429` or empty upstream output
|
### Provider returns `429` or empty upstream output
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ function showHelp(): number {
|
|||||||
console.log(' activate [profile] Print shell exports for the running proxy');
|
console.log(' activate [profile] Print shell exports for the running proxy');
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('Options:');
|
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(' --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(' --shell <name> activate only: auto|bash|zsh|fish|powershell');
|
||||||
console.log(' --fish activate only: shorthand for --shell fish');
|
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 portValue = parseOptionValue(args, '--port');
|
||||||
const host = parseOptionValue(args, '--host');
|
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;
|
let profile;
|
||||||
try {
|
try {
|
||||||
profile = resolveProfile(profileName);
|
profile = resolveProfile(profileName);
|
||||||
|
|||||||
@@ -1006,7 +1006,6 @@ export const DEFAULT_CLIPROXY_SERVER_CONFIG: CliproxyServerConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_OPENAI_COMPAT_PROXY_CONFIG: OpenAICompatProxyConfig = {
|
export const DEFAULT_OPENAI_COMPAT_PROXY_CONFIG: OpenAICompatProxyConfig = {
|
||||||
port: 3456,
|
|
||||||
profile_ports: {},
|
profile_ports: {},
|
||||||
routing: {
|
routing: {
|
||||||
longContextThreshold: 60_000,
|
longContextThreshold: 60_000,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { loadSettings } from '../utils/config-manager';
|
import { loadSettings } from '../utils/config-manager';
|
||||||
import { resolveOpenAICompatProfileConfig } from './profile-router';
|
import { resolveOpenAICompatProfileConfig } from './profile-router';
|
||||||
|
import { OPENAI_COMPAT_PROXY_DEFAULT_PORT } from './proxy-daemon-paths';
|
||||||
import { startOpenAICompatProxyServer } from './server/proxy-server';
|
import { startOpenAICompatProxyServer } from './server/proxy-server';
|
||||||
|
|
||||||
interface RuntimeOptions {
|
interface RuntimeOptions {
|
||||||
@@ -12,7 +13,7 @@ interface RuntimeOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseArgs(argv: string[]): RuntimeOptions {
|
function parseArgs(argv: string[]): RuntimeOptions {
|
||||||
let port = 3456;
|
let port = OPENAI_COMPAT_PROXY_DEFAULT_PORT;
|
||||||
let host = '127.0.0.1';
|
let host = '127.0.0.1';
|
||||||
let profileName = '';
|
let profileName = '';
|
||||||
let settingsPath = '';
|
let settingsPath = '';
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { getCcsDir } from '../utils/config-manager';
|
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 const OPENAI_COMPAT_PROXY_SERVICE_NAME = 'ccs-openai-compat-proxy';
|
||||||
|
|
||||||
export function getOpenAICompatProxyDir(): string {
|
export function getOpenAICompatProxyDir(): string {
|
||||||
|
|||||||
+28
-25
@@ -6,7 +6,8 @@ import * as lockfile from 'proper-lockfile';
|
|||||||
import { verifyProcessOwnership } from '../cursor/daemon-process-ownership';
|
import { verifyProcessOwnership } from '../cursor/daemon-process-ownership';
|
||||||
import type { OpenAICompatProfileConfig } from './profile-router';
|
import type { OpenAICompatProfileConfig } from './profile-router';
|
||||||
import {
|
import {
|
||||||
OPENAI_COMPAT_PROXY_DEFAULT_PORT,
|
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_END,
|
||||||
|
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_START,
|
||||||
OPENAI_COMPAT_PROXY_SERVICE_NAME,
|
OPENAI_COMPAT_PROXY_SERVICE_NAME,
|
||||||
getOpenAICompatProxyDir,
|
getOpenAICompatProxyDir,
|
||||||
} from './proxy-daemon-paths';
|
} from './proxy-daemon-paths';
|
||||||
@@ -25,7 +26,10 @@ import {
|
|||||||
writeOpenAICompatProxyPid,
|
writeOpenAICompatProxyPid,
|
||||||
writeOpenAICompatProxySession,
|
writeOpenAICompatProxySession,
|
||||||
} from './proxy-daemon-state';
|
} 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> {
|
export interface OpenAICompatProxyStatus extends Partial<OpenAICompatProxySession> {
|
||||||
running: boolean;
|
running: boolean;
|
||||||
@@ -131,6 +135,7 @@ async function terminateDaemonProcess(pid?: number): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function listOpenAICompatProxyCandidatePorts(
|
function listOpenAICompatProxyCandidatePorts(
|
||||||
|
profileName: string,
|
||||||
preferredPort: number,
|
preferredPort: number,
|
||||||
exact: boolean,
|
exact: boolean,
|
||||||
excludedPorts: ReadonlySet<number> = new Set()
|
excludedPorts: ReadonlySet<number> = new Set()
|
||||||
@@ -139,22 +144,7 @@ function listOpenAICompatProxyCandidatePorts(
|
|||||||
return excludedPorts.has(preferredPort) ? [] : [preferredPort];
|
return excludedPorts.has(preferredPort) ? [] : [preferredPort];
|
||||||
}
|
}
|
||||||
|
|
||||||
const candidates = new Set<number>();
|
return listFlexibleOpenAICompatProxyCandidatePorts(profileName, preferredPort, excludedPorts);
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPortBindConflictMessage(message?: string): boolean {
|
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 host = options.host?.trim() || status.host || '127.0.0.1';
|
||||||
const portPreference = resolveOpenAICompatProxyPortPreference(profile.profileName);
|
const portPreference = resolveOpenAICompatProxyPortPreference(profile.profileName);
|
||||||
const explicitPort = typeof options.port === 'number' ? options.port : undefined;
|
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';
|
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) {
|
if (status.running && status.port === preferredPort && (status.host || '127.0.0.1') === host) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -670,6 +672,7 @@ export async function startOpenAICompatProxy(
|
|||||||
const attemptedPorts = new Set<number>();
|
const attemptedPorts = new Set<number>();
|
||||||
let lastResult: OpenAICompatProxyLaunchResult | null = null;
|
let lastResult: OpenAICompatProxyLaunchResult | null = null;
|
||||||
const candidates = listOpenAICompatProxyCandidatePorts(
|
const candidates = listOpenAICompatProxyCandidatePorts(
|
||||||
|
profile.profileName,
|
||||||
preferredPort,
|
preferredPort,
|
||||||
requiresExactPort,
|
requiresExactPort,
|
||||||
attemptedPorts
|
attemptedPorts
|
||||||
@@ -678,7 +681,7 @@ export async function startOpenAICompatProxy(
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
port: preferredPort,
|
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,
|
port: preferredPort,
|
||||||
error: requiresExactPort
|
error: requiresExactPort
|
||||||
? `Requested proxy port ${preferredPort} is already in use`
|
? `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 { 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 {
|
export interface OpenAICompatProxyPortPreference {
|
||||||
port: number;
|
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(
|
export function resolveOpenAICompatProxyPortPreference(
|
||||||
@@ -14,9 +60,19 @@ export function resolveOpenAICompatProxyPortPreference(
|
|||||||
if (typeof profilePort === 'number') {
|
if (typeof profilePort === 'number') {
|
||||||
return { port: profilePort, source: 'profile' };
|
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 {
|
return {
|
||||||
port: config.proxy?.port ?? OPENAI_COMPAT_PROXY_DEFAULT_PORT,
|
port: resolveOpenAICompatProxyAdaptivePort(profileName),
|
||||||
source: 'default',
|
source: 'adaptive',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,27 @@ function runCli(args: string[], extraEnv: Record<string, string> = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writeJson(filePath: string, data: unknown) {
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProfileConfig(profiles: Record<string, Record<string, string>>) {
|
||||||
|
const ccsDir = path.join(tempDir, '.ccs');
|
||||||
|
fs.mkdirSync(ccsDir, { recursive: true });
|
||||||
|
const configProfiles = Object.fromEntries(
|
||||||
|
Object.keys(profiles).map((name) => [name, path.join(ccsDir, `${name}.settings.json`)])
|
||||||
|
);
|
||||||
|
writeJson(path.join(ccsDir, 'config.json'), { profiles: configProfiles });
|
||||||
|
for (const [name, env] of Object.entries(profiles)) {
|
||||||
|
writeJson(configProfiles[name], { env });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRunningPort(statusOutput: string) {
|
||||||
|
const match = statusOutput.match(/Local URL: http:\/\/127\.0\.0\.1:(\d+)/);
|
||||||
|
expect(match).not.toBeNull();
|
||||||
|
return Number(match?.[1]);
|
||||||
|
}
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
if (process.env.CCS_E2E_SKIP_BUILD === '1') {
|
if (process.env.CCS_E2E_SKIP_BUILD === '1') {
|
||||||
expect(fs.existsSync(DIST_ENTRY)).toBe(true);
|
expect(fs.existsSync(DIST_ENTRY)).toBe(true);
|
||||||
@@ -54,6 +75,8 @@ describe('proxy command e2e', () => {
|
|||||||
expect(help.status).toBe(0);
|
expect(help.status).toBe(0);
|
||||||
expect(help.stdout).toContain('Usage: ccs proxy <start|stop|status|activate> [profile] [options]');
|
expect(help.stdout).toContain('Usage: ccs proxy <start|stop|status|activate> [profile] [options]');
|
||||||
expect(help.stdout).toContain('stop [profile] Stop the running proxy (or all proxies when omitted)');
|
expect(help.stdout).toContain('stop [profile] Stop the running proxy (or all proxies when omitted)');
|
||||||
|
expect(help.stdout).toContain('Pin an exact local proxy port for this launch');
|
||||||
|
expect(help.stdout).not.toContain('default: 3456');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows the last-known proxy state when no proxy is currently running', async () => {
|
it('shows the last-known proxy state when no proxy is currently running', async () => {
|
||||||
@@ -84,45 +107,29 @@ describe('proxy command e2e', () => {
|
|||||||
expect(status.stdout).toContain('Profile: stale');
|
expect(status.stdout).toContain('Profile: stale');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('starts, reports status, activates, and stops via the built CLI', async () => {
|
it('surfaces the actual adaptive port in status and activation output', async () => {
|
||||||
const port = await getPort();
|
createProfileConfig({
|
||||||
const ccsDir = path.join(tempDir, '.ccs');
|
hf: {
|
||||||
fs.mkdirSync(ccsDir, { recursive: true });
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:11434',
|
||||||
const settingsPath = path.join(ccsDir, 'hf.settings.json');
|
ANTHROPIC_AUTH_TOKEN: 'ollama',
|
||||||
fs.writeFileSync(
|
ANTHROPIC_MODEL: 'qwen3-coder',
|
||||||
path.join(ccsDir, 'config.json'),
|
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||||
JSON.stringify({ profiles: { hf: settingsPath } }, null, 2),
|
},
|
||||||
'utf8'
|
});
|
||||||
);
|
expect(runCli(['proxy', 'start', 'hf', '--host', '127.0.0.1']).status).toBe(0);
|
||||||
fs.writeFileSync(
|
const status = runCli(['proxy', 'status', 'hf']);
|
||||||
settingsPath,
|
expect(status.status).toBe(0);
|
||||||
JSON.stringify({
|
const port = getRunningPort(status.stdout);
|
||||||
env: {
|
|
||||||
ANTHROPIC_BASE_URL: 'http://127.0.0.1:11434',
|
|
||||||
ANTHROPIC_AUTH_TOKEN: 'ollama',
|
|
||||||
ANTHROPIC_MODEL: 'qwen3-coder',
|
|
||||||
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
|
|
||||||
const started = runCli(['proxy', 'start', 'hf', '--port', String(port), '--host', '127.0.0.1']);
|
|
||||||
expect(started.status).toBe(0);
|
|
||||||
|
|
||||||
const status = runCli(['proxy', 'status']);
|
|
||||||
expect(status.stdout).toContain(`Proxy running on port ${port}`);
|
expect(status.stdout).toContain(`Proxy running on port ${port}`);
|
||||||
expect(status.stdout).toContain('Host: 127.0.0.1');
|
expect(status.stdout).toContain('Host: 127.0.0.1');
|
||||||
expect(status.stdout).toContain('Profile: hf');
|
expect(status.stdout).toContain('Profile: hf');
|
||||||
|
const activate = runCli(['proxy', 'activate', 'hf', '--shell', 'bash']);
|
||||||
const activate = runCli(['proxy', 'activate', '--shell', 'bash']);
|
|
||||||
expect(activate.stdout).toContain(`export ANTHROPIC_BASE_URL='http://127.0.0.1:${port}'`);
|
expect(activate.stdout).toContain(`export ANTHROPIC_BASE_URL='http://127.0.0.1:${port}'`);
|
||||||
expect(activate.stdout).toMatch(/export ANTHROPIC_AUTH_TOKEN='[a-f0-9]{48}'/);
|
expect(activate.stdout).toMatch(/export ANTHROPIC_AUTH_TOKEN='[a-f0-9]{48}'/);
|
||||||
expect(activate.stdout).toContain("export DISABLE_TELEMETRY='1'");
|
expect(activate.stdout).toContain("export DISABLE_TELEMETRY='1'");
|
||||||
expect(activate.stdout).toContain("export DISABLE_COST_WARNINGS='1'");
|
expect(activate.stdout).toContain("export DISABLE_COST_WARNINGS='1'");
|
||||||
expect(activate.stdout).toContain("export API_TIMEOUT_MS='600000'");
|
expect(activate.stdout).toContain("export API_TIMEOUT_MS='600000'");
|
||||||
expect(activate.stdout).toContain("export NO_PROXY='127.0.0.1,localhost'");
|
expect(activate.stdout).toContain("export NO_PROXY='127.0.0.1,localhost'");
|
||||||
|
|
||||||
const activateFish = runCli(['proxy', 'activate', '--fish']);
|
const activateFish = runCli(['proxy', 'activate', '--fish']);
|
||||||
expect(activateFish.stdout).toContain(`set -gx ANTHROPIC_BASE_URL 'http://127.0.0.1:${port}'`);
|
expect(activateFish.stdout).toContain(`set -gx ANTHROPIC_BASE_URL 'http://127.0.0.1:${port}'`);
|
||||||
|
|
||||||
@@ -147,41 +154,54 @@ describe('proxy command e2e', () => {
|
|||||||
expect(stopped.status).toBe(0);
|
expect(stopped.status).toBe(0);
|
||||||
}, 35000);
|
}, 35000);
|
||||||
|
|
||||||
|
it('respects an explicit --port override in status output', async () => {
|
||||||
|
const port = await getPort();
|
||||||
|
createProfileConfig({
|
||||||
|
hf: {
|
||||||
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:11434',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'ollama',
|
||||||
|
ANTHROPIC_MODEL: 'qwen3-coder',
|
||||||
|
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const started = runCli(['proxy', 'start', 'hf', '--port', String(port), '--host', '127.0.0.1']);
|
||||||
|
expect(started.status).toBe(0);
|
||||||
|
const status = runCli(['proxy', 'status', 'hf']);
|
||||||
|
expect(status.status).toBe(0);
|
||||||
|
expect(status.stdout).toContain(`Proxy running on port ${port}`);
|
||||||
|
expect(status.stdout).toContain(`Local URL: http://127.0.0.1:${port}`);
|
||||||
|
}, 35000);
|
||||||
|
|
||||||
|
it('rejects malformed explicit port values instead of coercing numeric prefixes', () => {
|
||||||
|
createProfileConfig({
|
||||||
|
hf: {
|
||||||
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:11434',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'ollama',
|
||||||
|
ANTHROPIC_MODEL: 'qwen3-coder',
|
||||||
|
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const invalid = runCli(['proxy', 'start', 'hf', '--port', '3456junk']);
|
||||||
|
expect(invalid.status).toBe(1);
|
||||||
|
expect(invalid.stderr).toContain('Invalid port: 3456junk');
|
||||||
|
});
|
||||||
|
|
||||||
it('requires an explicit profile when activating with multiple running proxies', async () => {
|
it('requires an explicit profile when activating with multiple running proxies', async () => {
|
||||||
const firstPort = await getPort();
|
const firstPort = await getPort();
|
||||||
const ccsDir = path.join(tempDir, '.ccs');
|
createProfileConfig({
|
||||||
fs.mkdirSync(ccsDir, { recursive: true });
|
ccg: {
|
||||||
const firstSettingsPath = path.join(ccsDir, 'ccg.settings.json');
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:11434',
|
||||||
const secondSettingsPath = path.join(ccsDir, 'ccgm.settings.json');
|
ANTHROPIC_AUTH_TOKEN: 'ollama-ccg',
|
||||||
fs.writeFileSync(
|
ANTHROPIC_MODEL: 'qwen3-coder',
|
||||||
path.join(ccsDir, 'config.json'),
|
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||||
JSON.stringify({ profiles: { ccg: firstSettingsPath, ccgm: secondSettingsPath } }, null, 2),
|
},
|
||||||
'utf8'
|
ccgm: {
|
||||||
);
|
ANTHROPIC_BASE_URL: 'https://api.openai.com/v1',
|
||||||
fs.writeFileSync(
|
ANTHROPIC_AUTH_TOKEN: 'sk-ccgm',
|
||||||
firstSettingsPath,
|
ANTHROPIC_MODEL: 'gpt-4.1',
|
||||||
JSON.stringify({
|
},
|
||||||
env: {
|
});
|
||||||
ANTHROPIC_BASE_URL: 'http://127.0.0.1:11434',
|
|
||||||
ANTHROPIC_AUTH_TOKEN: 'ollama-ccg',
|
|
||||||
ANTHROPIC_MODEL: 'qwen3-coder',
|
|
||||||
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
fs.writeFileSync(
|
|
||||||
secondSettingsPath,
|
|
||||||
JSON.stringify({
|
|
||||||
env: {
|
|
||||||
ANTHROPIC_BASE_URL: 'https://api.openai.com/v1',
|
|
||||||
ANTHROPIC_AUTH_TOKEN: 'sk-ccgm',
|
|
||||||
ANTHROPIC_MODEL: 'gpt-4.1',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(runCli(['proxy', 'start', 'ccg', '--port', String(firstPort)]).status).toBe(0);
|
expect(runCli(['proxy', 'start', 'ccg', '--port', String(firstPort)]).status).toBe(0);
|
||||||
const secondPort = await getPort();
|
const secondPort = await getPort();
|
||||||
expect(runCli(['proxy', 'start', 'ccgm', '--port', String(secondPort)]).status).toBe(0);
|
expect(runCli(['proxy', 'start', 'ccgm', '--port', String(secondPort)]).status).toBe(0);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
startOpenAICompatProxy,
|
startOpenAICompatProxy,
|
||||||
stopOpenAICompatProxy,
|
stopOpenAICompatProxy,
|
||||||
} from '../../../src/proxy/proxy-daemon';
|
} from '../../../src/proxy/proxy-daemon';
|
||||||
|
import { resolveOpenAICompatProxyPreferredPort } from '../../../src/proxy/proxy-port-resolver';
|
||||||
import { resolveOpenAICompatProfileConfig } from '../../../src/proxy/profile-router';
|
import { resolveOpenAICompatProfileConfig } from '../../../src/proxy/profile-router';
|
||||||
import {
|
import {
|
||||||
getLegacyOpenAICompatProxyPidPath,
|
getLegacyOpenAICompatProxyPidPath,
|
||||||
@@ -159,6 +160,34 @@ describe('openai proxy daemon lifecycle', () => {
|
|||||||
expect(secondHealth.status).toBe(200);
|
expect(secondHealth.status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses an adaptive implicit port instead of defaulting to 3456 for shared defaults', async () => {
|
||||||
|
const settingsPath = path.join(tempDir, 'adaptive-default.settings.json');
|
||||||
|
fs.writeFileSync(
|
||||||
|
settingsPath,
|
||||||
|
JSON.stringify({
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: 'https://api.openai.com/v1',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'sk-adaptive-default',
|
||||||
|
ANTHROPIC_MODEL: 'gpt-4.1',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const profile = resolveOpenAICompatProfileConfig('adaptive-default', settingsPath, {
|
||||||
|
ANTHROPIC_BASE_URL: 'https://api.openai.com/v1',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'sk-adaptive-default',
|
||||||
|
ANTHROPIC_MODEL: 'gpt-4.1',
|
||||||
|
});
|
||||||
|
if (!profile) {
|
||||||
|
throw new Error('Expected adaptive-default OpenAI-compatible profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
const started = await startOpenAICompatProxy(profile);
|
||||||
|
expect(started.success).toBe(true);
|
||||||
|
expect(started.port).not.toBe(3456);
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps a legacy singleton daemon visible across upgrade', async () => {
|
it('keeps a legacy singleton daemon visible across upgrade', async () => {
|
||||||
const port = await getPort();
|
const port = await getPort();
|
||||||
const settingsPath = path.join(tempDir, 'legacy.settings.json');
|
const settingsPath = path.join(tempDir, 'legacy.settings.json');
|
||||||
@@ -317,6 +346,54 @@ describe('openai proxy daemon lifecycle', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('falls back when a configured shared proxy.port is 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 ?? {}),
|
||||||
|
port: occupiedPort,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const settingsPath = path.join(tempDir, 'shared-port.settings.json');
|
||||||
|
fs.writeFileSync(
|
||||||
|
settingsPath,
|
||||||
|
JSON.stringify({
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:11434',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'ollama-shared-port',
|
||||||
|
ANTHROPIC_MODEL: 'qwen3-coder',
|
||||||
|
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const profile = resolveOpenAICompatProfileConfig('shared-port', settingsPath, {
|
||||||
|
ANTHROPIC_BASE_URL: 'http://127.0.0.1:11434',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'ollama-shared-port',
|
||||||
|
ANTHROPIC_MODEL: 'qwen3-coder',
|
||||||
|
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
|
||||||
|
});
|
||||||
|
if (!profile) {
|
||||||
|
throw new Error('Expected shared-port OpenAI-compatible profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
const started = await startOpenAICompatProxy(profile);
|
||||||
|
expect(started.success).toBe(true);
|
||||||
|
expect(started.port).not.toBe(occupiedPort);
|
||||||
|
} finally {
|
||||||
|
server.stop(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps the existing proxy running if replacement startup fails', async () => {
|
it('keeps the existing proxy running if replacement startup fails', async () => {
|
||||||
const firstPort = await getPort();
|
const firstPort = await getPort();
|
||||||
const occupiedPort = await getPort();
|
const occupiedPort = await getPort();
|
||||||
@@ -366,8 +443,8 @@ describe('openai proxy daemon lifecycle', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reuses the last-known port even when it is outside the default fallback range', async () => {
|
it('returns to the adaptive canonical port after a stale fallback session', async () => {
|
||||||
const preferredPort = await getPort();
|
const stalePort = await getPort();
|
||||||
const settingsPath = path.join(tempDir, 'outside-range.settings.json');
|
const settingsPath = path.join(tempDir, 'outside-range.settings.json');
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
settingsPath,
|
settingsPath,
|
||||||
@@ -400,7 +477,7 @@ describe('openai proxy daemon lifecycle', () => {
|
|||||||
profileName: profile.profileName,
|
profileName: profile.profileName,
|
||||||
settingsPath: profile.settingsPath,
|
settingsPath: profile.settingsPath,
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: preferredPort,
|
port: stalePort,
|
||||||
baseUrl: profile.baseUrl,
|
baseUrl: profile.baseUrl,
|
||||||
authToken: 'stale-token',
|
authToken: 'stale-token',
|
||||||
model: profile.model,
|
model: profile.model,
|
||||||
@@ -413,7 +490,57 @@ describe('openai proxy daemon lifecycle', () => {
|
|||||||
|
|
||||||
const started = await startOpenAICompatProxy(profile);
|
const started = await startOpenAICompatProxy(profile);
|
||||||
expect(started.success).toBe(true);
|
expect(started.success).toBe(true);
|
||||||
expect(started.port).toBe(preferredPort);
|
expect(started.port).toBe(resolveOpenAICompatProxyPreferredPort('outside-range'));
|
||||||
|
expect(started.port).not.toBe(stalePort);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not keep a stopped shared-default profile anchored to legacy port 3456', async () => {
|
||||||
|
const settingsPath = path.join(tempDir, 'legacy-shared-default.settings.json');
|
||||||
|
fs.writeFileSync(
|
||||||
|
settingsPath,
|
||||||
|
JSON.stringify({
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: 'https://api.openai.com/v1',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'sk-legacy-shared-default',
|
||||||
|
ANTHROPIC_MODEL: 'gpt-4.1',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const profile = resolveOpenAICompatProfileConfig('legacy-shared-default', settingsPath, {
|
||||||
|
ANTHROPIC_BASE_URL: 'https://api.openai.com/v1',
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'sk-legacy-shared-default',
|
||||||
|
ANTHROPIC_MODEL: 'gpt-4.1',
|
||||||
|
});
|
||||||
|
if (!profile) {
|
||||||
|
throw new Error('Expected legacy-shared-default OpenAI-compatible profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(getOpenAICompatProxySessionPath('legacy-shared-default')), {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
fs.writeFileSync(
|
||||||
|
getOpenAICompatProxySessionPath('legacy-shared-default'),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
profileName: profile.profileName,
|
||||||
|
settingsPath: profile.settingsPath,
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 3456,
|
||||||
|
baseUrl: profile.baseUrl,
|
||||||
|
authToken: 'stale-token',
|
||||||
|
model: profile.model,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
) + '\n',
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const started = await startOpenAICompatProxy(profile);
|
||||||
|
expect(started.success).toBe(true);
|
||||||
|
expect(started.port).not.toBe(3456);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stops legacy daemons even when the legacy session is missing a profile name', async () => {
|
it('stops legacy daemons even when the legacy session is missing a profile name', async () => {
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import * as fs from 'fs';
|
|||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { mutateUnifiedConfig } from '../../../src/config/unified-config-loader';
|
import { mutateUnifiedConfig } from '../../../src/config/unified-config-loader';
|
||||||
import { resolveOpenAICompatProxyPreferredPort } from '../../../src/proxy/proxy-port-resolver';
|
import {
|
||||||
|
resolveOpenAICompatProxyAdaptivePort,
|
||||||
|
resolveOpenAICompatProxyPortPreference,
|
||||||
|
resolveOpenAICompatProxyPreferredPort,
|
||||||
|
} from '../../../src/proxy/proxy-port-resolver';
|
||||||
|
|
||||||
let originalCcsHome: string | undefined;
|
let originalCcsHome: string | undefined;
|
||||||
let tempDir: string;
|
let tempDir: string;
|
||||||
@@ -36,7 +40,69 @@ describe('resolveOpenAICompatProxyPreferredPort', () => {
|
|||||||
expect(resolveOpenAICompatProxyPreferredPort('ccgm')).toBe(3461);
|
expect(resolveOpenAICompatProxyPreferredPort('ccgm')).toBe(3461);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to the shared default port when no profile mapping exists', () => {
|
it('returns a configured shared proxy port as a shared preference', () => {
|
||||||
expect(resolveOpenAICompatProxyPreferredPort('ccg')).toBe(3456);
|
mutateUnifiedConfig((config) => {
|
||||||
|
config.proxy = {
|
||||||
|
...(config.proxy ?? {}),
|
||||||
|
port: 45_000,
|
||||||
|
profile_ports: {},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolveOpenAICompatProxyPortPreference('ccg')).toEqual({
|
||||||
|
port: 45_000,
|
||||||
|
source: 'shared',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats legacy shared 3456 as an adaptive default on read', () => {
|
||||||
|
mutateUnifiedConfig((config) => {
|
||||||
|
config.proxy = {
|
||||||
|
...(config.proxy ?? {}),
|
||||||
|
port: 3456,
|
||||||
|
profile_ports: {},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolveOpenAICompatProxyPortPreference('ccg')).toEqual({
|
||||||
|
port: resolveOpenAICompatProxyAdaptivePort('ccg'),
|
||||||
|
source: 'adaptive',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves an explicit shared 43456 port when the user configures it', () => {
|
||||||
|
mutateUnifiedConfig((config) => {
|
||||||
|
config.proxy = {
|
||||||
|
...(config.proxy ?? {}),
|
||||||
|
port: 43_456,
|
||||||
|
profile_ports: {},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolveOpenAICompatProxyPortPreference('ccg')).toEqual({
|
||||||
|
port: 43_456,
|
||||||
|
source: 'shared',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to an adaptive shared default when no profile mapping exists', () => {
|
||||||
|
const portPreference = resolveOpenAICompatProxyPortPreference('ccg');
|
||||||
|
|
||||||
|
expect(portPreference).toEqual({
|
||||||
|
port: resolveOpenAICompatProxyAdaptivePort('ccg'),
|
||||||
|
source: 'adaptive',
|
||||||
|
});
|
||||||
|
expect(resolveOpenAICompatProxyPreferredPort('ccg')).not.toBe(3456);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('derives a stable adaptive default that does not keep all profiles on 3456', () => {
|
||||||
|
const first = resolveOpenAICompatProxyPreferredPort('ccg');
|
||||||
|
const second = resolveOpenAICompatProxyPreferredPort('ccg');
|
||||||
|
const other = resolveOpenAICompatProxyPreferredPort('ccgm');
|
||||||
|
|
||||||
|
expect(first).toBe(second);
|
||||||
|
expect(first).not.toBe(3456);
|
||||||
|
expect(other).not.toBe(3456);
|
||||||
|
expect(other).not.toBe(first);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user