mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 04:17:54 +00:00
fix(proxy): clarify shared port fallback and legacy 3456 handling
This commit is contained in:
@@ -79,10 +79,16 @@ ccs proxy status hf
|
|||||||
ccs proxy stop hf
|
ccs proxy stop hf
|
||||||
```
|
```
|
||||||
|
|
||||||
By default, CCS picks a deterministic local port for each compatible profile
|
Port selection precedence is:
|
||||||
and adapts automatically when that port is unavailable. Use `--port` for a
|
|
||||||
one-off pinned port, or set `proxy.profile_ports` in config when you want a
|
1. CLI `--port` for an exact one-off pin
|
||||||
stable reserved port per profile.
|
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:
|
||||||
|
|
||||||
@@ -107,10 +113,11 @@ runtime as a singleton.
|
|||||||
- `status` and `activate` always reflect the actual running port instead of an
|
- `status` and `activate` always reflect the actual running port instead of an
|
||||||
assumed default
|
assumed default
|
||||||
|
|
||||||
If you want to pin ports explicitly, 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: 45000
|
||||||
profile_ports:
|
profile_ports:
|
||||||
hf: 3460
|
hf: 3460
|
||||||
openai: 3461
|
openai: 3461
|
||||||
|
|||||||
@@ -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> Pin an exact local proxy port (default: adaptive)');
|
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');
|
||||||
|
|||||||
+17
-10
@@ -8,7 +8,6 @@ import type { OpenAICompatProfileConfig } from './profile-router';
|
|||||||
import {
|
import {
|
||||||
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_END,
|
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_END,
|
||||||
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_START,
|
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_START,
|
||||||
OPENAI_COMPAT_PROXY_LEGACY_DEFAULT_PORT,
|
|
||||||
OPENAI_COMPAT_PROXY_SERVICE_NAME,
|
OPENAI_COMPAT_PROXY_SERVICE_NAME,
|
||||||
getOpenAICompatProxyDir,
|
getOpenAICompatProxyDir,
|
||||||
} from './proxy-daemon-paths';
|
} from './proxy-daemon-paths';
|
||||||
@@ -485,16 +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 rawPreferredPort =
|
|
||||||
explicitPort ??
|
|
||||||
(portPreference.source === 'profile'
|
|
||||||
? portPreference.port
|
|
||||||
: portPreference.source === 'adaptive' &&
|
|
||||||
status.port === OPENAI_COMPAT_PROXY_LEGACY_DEFAULT_PORT
|
|
||||||
? portPreference.port
|
|
||||||
: status.port || portPreference.port);
|
|
||||||
const preferredPort = rawPreferredPort;
|
|
||||||
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,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
|
|||||||
import {
|
import {
|
||||||
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_END,
|
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_END,
|
||||||
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_START,
|
OPENAI_COMPAT_PROXY_ADAPTIVE_PORT_START,
|
||||||
|
OPENAI_COMPAT_PROXY_LEGACY_DEFAULT_PORT,
|
||||||
} from './proxy-daemon-paths';
|
} from './proxy-daemon-paths';
|
||||||
|
|
||||||
export interface OpenAICompatProxyPortPreference {
|
export interface OpenAICompatProxyPortPreference {
|
||||||
@@ -61,6 +62,12 @@ export function resolveOpenAICompatProxyPortPreference(
|
|||||||
}
|
}
|
||||||
const sharedPort = config.proxy?.port;
|
const sharedPort = config.proxy?.port;
|
||||||
if (typeof sharedPort === 'number') {
|
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: sharedPort, source: 'shared' };
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ 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');
|
expect(help.stdout).toContain('Pin an exact local proxy port for this launch');
|
||||||
expect(help.stdout).not.toContain('default: 3456');
|
expect(help.stdout).not.toContain('default: 3456');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -345,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();
|
||||||
@@ -394,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,
|
||||||
@@ -428,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,
|
||||||
@@ -441,7 +490,8 @@ 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 () => {
|
it('does not keep a stopped shared-default profile anchored to legacy port 3456', async () => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import * as path from 'path';
|
|||||||
import { mutateUnifiedConfig } from '../../../src/config/unified-config-loader';
|
import { mutateUnifiedConfig } from '../../../src/config/unified-config-loader';
|
||||||
import {
|
import {
|
||||||
resolveOpenAICompatProxyAdaptivePort,
|
resolveOpenAICompatProxyAdaptivePort,
|
||||||
|
resolveOpenAICompatProxyPortPreference,
|
||||||
resolveOpenAICompatProxyPreferredPort,
|
resolveOpenAICompatProxyPreferredPort,
|
||||||
} from '../../../src/proxy/proxy-port-resolver';
|
} from '../../../src/proxy/proxy-port-resolver';
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ describe('resolveOpenAICompatProxyPreferredPort', () => {
|
|||||||
expect(resolveOpenAICompatProxyPreferredPort('ccgm')).toBe(3461);
|
expect(resolveOpenAICompatProxyPreferredPort('ccgm')).toBe(3461);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('preserves an explicit shared proxy port outside the adaptive default path', () => {
|
it('returns a configured shared proxy port as a shared preference', () => {
|
||||||
mutateUnifiedConfig((config) => {
|
mutateUnifiedConfig((config) => {
|
||||||
config.proxy = {
|
config.proxy = {
|
||||||
...(config.proxy ?? {}),
|
...(config.proxy ?? {}),
|
||||||
@@ -48,10 +49,13 @@ describe('resolveOpenAICompatProxyPreferredPort', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(resolveOpenAICompatProxyPreferredPort('ccg')).toBe(45_000);
|
expect(resolveOpenAICompatProxyPortPreference('ccg')).toEqual({
|
||||||
|
port: 45_000,
|
||||||
|
source: 'shared',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('preserves an explicit shared legacy 3456 port when the user configures it', () => {
|
it('treats legacy shared 3456 as an adaptive default on read', () => {
|
||||||
mutateUnifiedConfig((config) => {
|
mutateUnifiedConfig((config) => {
|
||||||
config.proxy = {
|
config.proxy = {
|
||||||
...(config.proxy ?? {}),
|
...(config.proxy ?? {}),
|
||||||
@@ -60,7 +64,10 @@ describe('resolveOpenAICompatProxyPreferredPort', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(resolveOpenAICompatProxyPreferredPort('ccg')).toBe(3456);
|
expect(resolveOpenAICompatProxyPortPreference('ccg')).toEqual({
|
||||||
|
port: resolveOpenAICompatProxyAdaptivePort('ccg'),
|
||||||
|
source: 'adaptive',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('preserves an explicit shared 43456 port when the user configures it', () => {
|
it('preserves an explicit shared 43456 port when the user configures it', () => {
|
||||||
@@ -72,14 +79,20 @@ describe('resolveOpenAICompatProxyPreferredPort', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(resolveOpenAICompatProxyPreferredPort('ccg')).toBe(43_456);
|
expect(resolveOpenAICompatProxyPortPreference('ccg')).toEqual({
|
||||||
|
port: 43_456,
|
||||||
|
source: 'shared',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to an adaptive shared default when no profile mapping exists', () => {
|
it('falls back to an adaptive shared default when no profile mapping exists', () => {
|
||||||
const preferredPort = resolveOpenAICompatProxyPreferredPort('ccg');
|
const portPreference = resolveOpenAICompatProxyPortPreference('ccg');
|
||||||
|
|
||||||
expect(preferredPort).toBe(resolveOpenAICompatProxyAdaptivePort('ccg'));
|
expect(portPreference).toEqual({
|
||||||
expect(preferredPort).not.toBe(3456);
|
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', () => {
|
it('derives a stable adaptive default that does not keep all profiles on 3456', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user