feat(proxy): add openai-compatible local proxy runtime

This commit is contained in:
Tam Nhu Tran
2026-04-14 19:26:59 -04:00
parent 9545499487
commit 074e900557
22 changed files with 1428 additions and 46 deletions
+52
View File
@@ -97,6 +97,11 @@ import {
} from './targets/droid-reasoning-runtime';
import { DroidCommandRouterError, routeDroidCommandArgs } from './targets/droid-command-router';
import { resolveCliproxyBridgeMetadata } from './api/services/cliproxy-profile-bridge';
import {
buildOpenAICompatProxyEnv,
resolveOpenAICompatProfileConfig,
startOpenAICompatProxy,
} from './proxy';
// Version and Update check utilities
import { getVersion } from './utils/version';
@@ -1304,6 +1309,53 @@ async function main(): Promise<void> {
const browserArgs = browserRuntimeEnv
? appendBrowserToolArgs(imageAnalysisArgs)
: imageAnalysisArgs;
const openAICompatProfile = resolveOpenAICompatProfileConfig(
profileInfo.name,
expandedSettingsPath,
settingsEnv
);
if (openAICompatProfile) {
const proxyStart = await startOpenAICompatProxy(openAICompatProfile, {
insecure: openAICompatProfile.insecure,
});
if (!proxyStart.success) {
console.error(fail(proxyStart.error || 'Failed to start local OpenAI-compatible proxy'));
process.exit(1);
}
console.error(
info(
`Using local OpenAI-compatible proxy for "${profileInfo.name}" on port ${proxyStart.port}`
)
);
const proxyEnv = {
...envVars,
...buildOpenAICompatProxyEnv(
openAICompatProfile,
proxyStart.port,
proxyStart.authToken || '',
inheritedClaudeConfigDir
),
};
delete proxyEnv.ANTHROPIC_API_KEY;
const launchArgs = [
'--settings',
expandedSettingsPath,
...appendThirdPartyWebSearchToolArgs(browserArgs),
];
const traceEnv = createWebSearchTraceContext({
launcher: 'ccs.settings-profile.proxy',
args: launchArgs,
profile: profileInfo.name,
profileType: profileInfo.type,
settingsPath: expandedSettingsPath,
});
execClaude(claudeCli, launchArgs, { ...proxyEnv, ...traceEnv });
return;
}
const launchArgs = [
'--settings',
expandedSettingsPath,
+7
View File
@@ -109,6 +109,12 @@ export const ROOT_COMMAND_CATALOG: readonly RootCommandEntry[] = [
group: 'runtime',
visibility: 'public',
},
{
name: 'proxy',
summary: 'Start or inspect the OpenAI-compatible local proxy',
group: 'runtime',
visibility: 'public',
},
{
name: 'copilot',
summary: 'Run or manage the GitHub Copilot bridge',
@@ -252,6 +258,7 @@ export const CLIPROXY_SUBCOMMANDS = [
] as const;
export const CONFIG_SUBCOMMANDS = ['auth', 'channels', 'image-analysis', 'thinking'] as const;
export const DOCKER_SUBCOMMANDS = ['up', 'down', 'status', 'update', 'logs', 'config'] as const;
export const PROXY_SUBCOMMANDS = ['start', 'stop', 'status', 'activate'] as const;
export const TOKENS_FLAGS = [
'--show',
'--api-key',
+8
View File
@@ -11,6 +11,7 @@ import {
ROOT_COMMAND_FLAGS,
ROOT_HELP_TOPICS,
TOKENS_FLAGS,
PROXY_SUBCOMMANDS,
PROVIDER_FLAGS,
uniqueStrings,
getPublicRootCommandTokens,
@@ -189,6 +190,13 @@ function getSuggestionsForCommand(tokensBeforeCurrent: string[]): CompletionSugg
return completeSubcommands([], COMMAND_FLAG_SUGGESTIONS.docker);
case 'cursor':
return completeSubcommands(CURSOR_COMPLETION_SUBCOMMANDS);
case 'proxy':
if (lastToken === '--shell')
return completeSubcommands(['auto', 'bash', 'zsh', 'fish', 'powershell']);
return completeSubcommands(
[...PROXY_SUBCOMMANDS],
['--port', '--shell', '--insecure', '--help', '-h']
);
case 'copilot':
return completeSubcommands(COPILOT_COMPLETION_SUBCOMMANDS);
case 'env':
+3
View File
@@ -215,6 +215,7 @@ export async function handleHelpCommand(writeLine: HelpWriter = console.log): Pr
name: 'ccs cliproxy --help',
summary: 'Deep help for variants, routing, quota, and lifecycle',
},
{ name: 'ccs proxy --help', summary: 'Deep help for the OpenAI-compatible local proxy' },
{ name: 'ccs docker --help', summary: 'Deep help for Docker deployment commands' },
{ name: 'ccs cursor --help', summary: 'Deep help for Cursor runtime/admin commands' },
{ name: 'ccs copilot --help', summary: 'Deep help for GitHub Copilot commands' },
@@ -275,6 +276,8 @@ export async function handleHelpRoute(
process.exit(await (await import('./copilot-command')).handleCopilotCommand(['--help'])),
cursor: async () =>
process.exit(await (await import('./cursor-command')).handleCursorCommand(['--help'])),
proxy: async () =>
process.exit(await (await import('./proxy-command')).handleProxyCommand(['--help'])),
docker: async () => (await import('./docker/help-subcommand')).showHelp(),
migrate: async () => (await import('./migrate-command')).printMigrateHelp(),
setup: async () => (await import('./setup-command')).handleSetupCommand(['--help']),
+1
View File
@@ -13,6 +13,7 @@ export { handleDockerCommand } from './docker-command';
export { handleHelpCommand } from './help-command';
export { handleInstallCommand } from './install-command';
export { handleMigrateCommand } from './migrate-command';
export { handleProxyCommand } from './proxy-command';
export { handleShellCompletionCommand } from './shell-completion-command';
export { handleSyncCommand } from './sync-command';
export { handleUpdateCommand } from './update-command';
+165
View File
@@ -0,0 +1,165 @@
import { detectShell, formatExportLine } from './env-command';
import { getSettingsPath, loadSettings } from '../utils/config-manager';
import { expandPath } from '../utils/helpers';
import { fail, info, ok } from '../utils/ui';
import {
buildOpenAICompatProxyEnv,
getOpenAICompatProxyStatus,
resolveOpenAICompatProfileConfig,
startOpenAICompatProxy,
stopOpenAICompatProxy,
} from '../proxy';
function parseOptionValue(args: string[], key: string): string | undefined {
const exactIndex = args.findIndex((arg) => arg === key);
if (exactIndex !== -1 && args[exactIndex + 1]) {
return args[exactIndex + 1];
}
const prefix = `${key}=`;
const withEquals = args.find((arg) => arg.startsWith(prefix));
return withEquals ? withEquals.slice(prefix.length) : undefined;
}
function showHelp(): number {
console.log('OpenAI-Compatible Proxy');
console.log('');
console.log('Usage: ccs proxy <start|stop|status|activate> [profile] [options]');
console.log('');
console.log('Commands:');
console.log(
' start <profile> Start the local proxy for an OpenAI-compatible settings profile'
);
console.log(' stop Stop the running proxy');
console.log(' status Show daemon status and active profile');
console.log(' activate 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(' --shell <name> activate only: auto|bash|zsh|fish|powershell');
console.log(' --insecure Disable upstream TLS verification');
console.log('');
console.log('Examples:');
console.log(' ccs proxy start hf');
console.log(' eval "$(ccs proxy activate)"');
console.log(' ccs proxy status');
console.log(' ccs proxy stop');
console.log('');
return 0;
}
function resolveProfile(profileName: string) {
const settingsPath = expandPath(getSettingsPath(profileName));
const settings = loadSettings(settingsPath);
const profile = resolveOpenAICompatProfileConfig(profileName, settingsPath, settings.env || {});
if (!profile) {
throw new Error(`Profile "${profileName}" is not configured for an OpenAI-compatible endpoint`);
}
return profile;
}
async function handleStart(args: string[]): Promise<number> {
const profileName = args.find((arg) => !arg.startsWith('-'));
if (!profileName) {
console.error(fail('Usage: ccs proxy start <profile> [--port <n>] [--insecure]'));
return 1;
}
const portValue = parseOptionValue(args, '--port');
const port = portValue ? Number.parseInt(portValue, 10) || 3456 : undefined;
let profile;
try {
profile = resolveProfile(profileName);
} catch (error) {
console.error(fail((error as Error).message));
return 1;
}
const result = await startOpenAICompatProxy(profile, {
...(port ? { port } : {}),
insecure: args.includes('--insecure'),
});
if (!result.success) {
console.error(fail(result.error || 'Failed to start proxy'));
return 1;
}
console.log(
result.alreadyRunning
? info(`Proxy already running on port ${result.port}`)
: ok(`Proxy started on port ${result.port}`)
);
return 0;
}
async function handleStatus(): Promise<number> {
const status = await getOpenAICompatProxyStatus();
if (!status.running) {
console.log(info('Proxy is not running'));
return 0;
}
console.log(ok(`Proxy running on port ${status.port}`));
console.log(` Profile: ${status.profileName}`);
console.log(` Base URL: ${status.baseUrl}`);
if (status.model) {
console.log(` Model: ${status.model}`);
}
if (status.pid) {
console.log(` PID: ${status.pid}`);
}
return 0;
}
async function handleActivate(args: string[]): Promise<number> {
const status = await getOpenAICompatProxyStatus();
if (!status.running || !status.profileName || !status.port || !status.authToken) {
console.error(fail('Proxy is not running. Start it with: ccs proxy start <profile>'));
return 1;
}
const shell = detectShell(parseOptionValue(args, '--shell'));
let profile;
try {
profile = resolveProfile(status.profileName);
} catch (error) {
console.error(fail((error as Error).message));
return 1;
}
const env = buildOpenAICompatProxyEnv(profile, status.port, status.authToken);
Object.entries(env).forEach(([key, value]) => {
console.log(formatExportLine(shell, key, value));
});
return 0;
}
export async function handleProxyCommand(args: string[]): Promise<number> {
const subcommand = args[0];
switch (subcommand) {
case undefined:
case 'help':
case '--help':
case '-h':
return showHelp();
case 'start':
return handleStart(args.slice(1));
case 'stop': {
const result = await stopOpenAICompatProxy();
if (!result.success) {
console.error(fail(result.error || 'Failed to stop proxy'));
return 1;
}
console.log(ok('Proxy stopped'));
return 0;
}
case 'status':
return handleStatus();
case 'activate':
return handleActivate(args.slice(1));
default:
console.error(fail(`Unknown proxy subcommand: ${subcommand}`));
return 1;
}
}
+7
View File
@@ -136,6 +136,13 @@ export const ROOT_COMMAND_ROUTES: readonly NamedCommandRoute[] = [
await handleCliproxyCommand(args);
},
},
{
name: 'proxy',
handle: async (args) => {
const { handleProxyCommand } = await import('./proxy-command');
process.exit(await handleProxyCommand(args));
},
},
{
name: 'docker',
handle: async (args) => {
+1
View File
@@ -17,6 +17,7 @@ export const RESERVED_PROFILE_NAMES = [
'default',
'config',
'cliproxy',
'proxy',
] as const;
export type ReservedProfileName = (typeof RESERVED_PROFILE_NAMES)[number];
+56 -46
View File
@@ -49,63 +49,73 @@ export class SSEParser {
* @returns Array of parsed events
*/
parse(chunk: Buffer | string): SSEEvent[] {
this.buffer += chunk.toString();
this.buffer += chunk.toString().replace(/\r\n/g, '\n');
// C-01 Fix: Prevent unbounded buffer growth (DoS protection)
if (this.buffer.length > this.maxBufferSize) {
throw new Error(`SSE buffer exceeded ${this.maxBufferSize} bytes (DoS protection)`);
}
const lines = this.buffer.split('\n');
// Keep incomplete line in buffer
this.buffer = lines.pop() || '';
const events: SSEEvent[] = [];
let currentEvent: SSEEvent = { event: 'message', data: '' };
const segments = this.buffer.split('\n\n');
this.buffer = segments.pop() || '';
for (const line of lines) {
if (line.startsWith('event: ')) {
currentEvent.event = line.substring(7).trim();
} else if (line.startsWith('data: ')) {
const data = line.substring(6);
for (const segment of segments) {
const lines = segment.split('\n');
const currentEvent: SSEEvent = { event: 'message', data: '' };
const dataLines: string[] = [];
if (data === '[DONE]') {
this.eventCount++;
events.push({
event: 'done',
data: null,
index: this.eventCount,
});
currentEvent = { event: 'message', data: '' };
} else {
try {
currentEvent.data = JSON.parse(data);
this.eventCount++;
currentEvent.index = this.eventCount;
events.push({ ...currentEvent });
currentEvent = { event: 'message', data: '' };
} catch (e) {
// H-01 Fix: Log parse errors for debugging
if (typeof console !== 'undefined' && console.error) {
console.error(
'[SSEParser] Malformed JSON event:',
(e as Error).message,
'Data:',
data.substring(0, 100)
);
}
if (this.throwOnMalformedJson) {
throw new Error(`Malformed SSE JSON event: ${(e as Error).message}`);
}
}
for (const rawLine of lines) {
const line = rawLine.trimEnd();
if (!line || line.startsWith(':')) {
continue;
}
if (line.startsWith('event: ')) {
currentEvent.event = line.substring(7).trim();
continue;
}
if (line.startsWith('data:')) {
dataLines.push(line.substring(5).trimStart());
continue;
}
if (line.startsWith('id: ')) {
currentEvent.id = line.substring(4).trim();
continue;
}
if (line.startsWith('retry: ')) {
currentEvent.retry = parseInt(line.substring(7), 10);
}
}
const data = dataLines.join('\n');
if (!data) {
continue;
}
if (data === '[DONE]') {
this.eventCount++;
events.push({ event: 'done', data: null, index: this.eventCount });
continue;
}
try {
currentEvent.data = JSON.parse(data);
this.eventCount++;
currentEvent.index = this.eventCount;
events.push({ ...currentEvent });
} catch (e) {
if (typeof console !== 'undefined' && console.error) {
console.error(
'[SSEParser] Malformed JSON event:',
(e as Error).message,
'Data:',
data.substring(0, 100)
);
}
if (this.throwOnMalformedJson) {
throw new Error(`Malformed SSE JSON event: ${(e as Error).message}`);
}
} else if (line.startsWith('id: ')) {
currentEvent.id = line.substring(4).trim();
} else if (line.startsWith('retry: ')) {
currentEvent.retry = parseInt(line.substring(7), 10);
}
// Empty lines separate events (already handled by JSON parsing)
}
return events;
+7
View File
@@ -0,0 +1,7 @@
export * from './profile-router';
export * from './proxy-daemon';
export * from './proxy-daemon-paths';
export * from './proxy-env';
export * from './upstream-url';
export * from './transformers/request-transformer';
export * from './transformers/sse-stream-transformer';
+70
View File
@@ -0,0 +1,70 @@
import { resolveDroidProvider, type DroidProvider } from '../targets/droid-provider';
export interface OpenAICompatProfileConfig {
profileName: string;
settingsPath: string;
baseUrl: string;
apiKey: string;
provider: DroidProvider;
insecure?: boolean;
model?: string;
opusModel?: string;
sonnetModel?: string;
haikuModel?: string;
}
export interface OpenAICompatProfileEnv {
ANTHROPIC_BASE_URL?: string;
ANTHROPIC_AUTH_TOKEN?: string;
ANTHROPIC_API_KEY?: string;
ANTHROPIC_MODEL?: string;
ANTHROPIC_DEFAULT_OPUS_MODEL?: string;
ANTHROPIC_DEFAULT_SONNET_MODEL?: string;
ANTHROPIC_DEFAULT_HAIKU_MODEL?: string;
ANTHROPIC_SMALL_FAST_MODEL?: string;
CCS_DROID_PROVIDER?: string;
CCS_OPENAI_PROXY_INSECURE?: string;
}
export function isOpenAICompatProvider(provider: DroidProvider | null): provider is DroidProvider {
return provider === 'openai' || provider === 'generic-chat-completion-api';
}
export function resolveOpenAICompatProfileConfig(
profileName: string,
settingsPath: string,
env: OpenAICompatProfileEnv
): OpenAICompatProfileConfig | null {
const baseUrl = env.ANTHROPIC_BASE_URL?.trim() || '';
const apiKey = env.ANTHROPIC_AUTH_TOKEN?.trim() || env.ANTHROPIC_API_KEY?.trim() || '';
if (!baseUrl || !apiKey) {
return null;
}
const provider = resolveDroidProvider({
provider: env.CCS_DROID_PROVIDER,
baseUrl,
model: env.ANTHROPIC_MODEL,
});
if (!isOpenAICompatProvider(provider)) {
return null;
}
return {
profileName,
settingsPath,
baseUrl,
apiKey,
provider,
insecure:
env.CCS_OPENAI_PROXY_INSECURE === '1' ||
env.CCS_OPENAI_PROXY_INSECURE?.toLowerCase() === 'true',
model: env.ANTHROPIC_MODEL?.trim() || undefined,
opusModel: env.ANTHROPIC_DEFAULT_OPUS_MODEL?.trim() || undefined,
sonnetModel: env.ANTHROPIC_DEFAULT_SONNET_MODEL?.trim() || undefined,
haikuModel:
env.ANTHROPIC_DEFAULT_HAIKU_MODEL?.trim() ||
env.ANTHROPIC_SMALL_FAST_MODEL?.trim() ||
undefined,
};
}
+80
View File
@@ -0,0 +1,80 @@
import { loadSettings } from '../utils/config-manager';
import { resolveOpenAICompatProfileConfig } from './profile-router';
import { startOpenAICompatProxyServer } from './server/proxy-server';
interface RuntimeOptions {
port: number;
profileName: string;
settingsPath: string;
authToken: string;
insecure: boolean;
}
function parseArgs(argv: string[]): RuntimeOptions {
let port = 3456;
let profileName = '';
let settingsPath = '';
let authToken = '';
let insecure = false;
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === '--port' && argv[i + 1]) {
port = Number.parseInt(argv[++i] || '', 10) || port;
continue;
}
if (arg === '--profile' && argv[i + 1]) {
profileName = argv[++i] || '';
continue;
}
if (arg === '--settings-path' && argv[i + 1]) {
settingsPath = argv[++i] || '';
continue;
}
if (arg === '--auth-token' && argv[i + 1]) {
authToken = argv[++i] || '';
continue;
}
if (arg === '--insecure') {
insecure = true;
}
}
return { port, profileName, settingsPath, authToken, insecure };
}
function startRuntime(options: RuntimeOptions): void {
if (!options.authToken.trim()) {
throw new Error('Missing local proxy auth token');
}
const settings = loadSettings(options.settingsPath);
const profile = resolveOpenAICompatProfileConfig(
options.profileName,
options.settingsPath,
settings.env || {}
);
if (!profile) {
throw new Error(
`Profile "${options.profileName}" is not an OpenAI-compatible settings profile`
);
}
const server = startOpenAICompatProxyServer({
profile,
port: options.port,
authToken: options.authToken,
insecure: options.insecure,
});
server.once('error', (error) => {
console.error((error as Error).message);
process.exit(1);
});
const shutdown = () => server.close();
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
}
if (require.main === module) {
startRuntime(parseArgs(process.argv.slice(2)));
}
+17
View File
@@ -0,0 +1,17 @@
import * as path from 'path';
import { getCcsDir } from '../utils/config-manager';
export const OPENAI_COMPAT_PROXY_DEFAULT_PORT = 3456;
export const OPENAI_COMPAT_PROXY_SERVICE_NAME = 'ccs-openai-compat-proxy';
export function getOpenAICompatProxyDir(): string {
return path.join(getCcsDir(), 'proxy');
}
export function getOpenAICompatProxyPidPath(): string {
return path.join(getOpenAICompatProxyDir(), 'daemon.pid');
}
export function getOpenAICompatProxySessionPath(): string {
return path.join(getOpenAICompatProxyDir(), 'session.json');
}
+82
View File
@@ -0,0 +1,82 @@
import * as fs from 'fs';
import * as path from 'path';
import {
getOpenAICompatProxyDir,
getOpenAICompatProxyPidPath,
getOpenAICompatProxySessionPath,
} from './proxy-daemon-paths';
export interface OpenAICompatProxySession {
profileName: string;
settingsPath: string;
port: number;
baseUrl: string;
authToken: string;
model?: string;
insecure?: boolean;
}
function ensureProxyDir(): void {
fs.mkdirSync(getOpenAICompatProxyDir(), { recursive: true });
}
export function getOpenAICompatProxyPid(): number | null {
try {
const raw = fs.readFileSync(getOpenAICompatProxyPidPath(), 'utf8').trim();
const pid = Number.parseInt(raw, 10);
return Number.isInteger(pid) ? pid : null;
} catch {
return null;
}
}
export function writeOpenAICompatProxyPid(pid: number): void {
ensureProxyDir();
fs.writeFileSync(getOpenAICompatProxyPidPath(), String(pid), 'utf8');
}
export function removeOpenAICompatProxyPid(): void {
try {
fs.unlinkSync(getOpenAICompatProxyPidPath());
} catch {
// Best-effort cleanup.
}
}
export function readOpenAICompatProxySession(): OpenAICompatProxySession | null {
try {
return JSON.parse(
fs.readFileSync(getOpenAICompatProxySessionPath(), 'utf8')
) as OpenAICompatProxySession;
} catch {
return null;
}
}
export function writeOpenAICompatProxySession(session: OpenAICompatProxySession): void {
ensureProxyDir();
fs.writeFileSync(
getOpenAICompatProxySessionPath(),
JSON.stringify(session, null, 2) + '\n',
'utf8'
);
}
export function removeOpenAICompatProxySession(): void {
try {
fs.unlinkSync(getOpenAICompatProxySessionPath());
} catch {
// Best-effort cleanup.
}
}
export function resolveOpenAICompatProxyEntrypointCandidates(): string[] {
const jsEntry = path.join(__dirname, 'proxy-daemon-entry.js');
const tsEntry = path.join(__dirname, 'proxy-daemon-entry.ts');
const isBunRuntime = process.execPath.toLowerCase().includes('bun');
const runningFromDist = __filename.endsWith('.js');
if (runningFromDist) {
return [jsEntry];
}
return isBunRuntime ? [tsEntry, jsEntry] : [jsEntry];
}
+380
View File
@@ -0,0 +1,380 @@
import { spawn, type ChildProcess } from 'child_process';
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as http from 'http';
import * as net from 'net';
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_SERVICE_NAME,
getOpenAICompatProxyDir,
} from './proxy-daemon-paths';
import {
getOpenAICompatProxyPid,
readOpenAICompatProxySession,
removeOpenAICompatProxyPid,
removeOpenAICompatProxySession,
resolveOpenAICompatProxyEntrypointCandidates,
type OpenAICompatProxySession,
writeOpenAICompatProxyPid,
writeOpenAICompatProxySession,
} from './proxy-daemon-state';
export interface OpenAICompatProxyStatus extends Partial<OpenAICompatProxySession> {
running: boolean;
pid?: number;
}
export interface StartOpenAICompatProxyResult {
success: boolean;
alreadyRunning?: boolean;
authToken?: string;
pid?: number;
port: number;
error?: string;
}
function generateProxyAuthToken(): string {
return crypto.randomBytes(24).toString('hex');
}
async function withOpenAICompatProxyLock<T>(operation: () => Promise<T>): Promise<T> {
const proxyDir = getOpenAICompatProxyDir();
await fs.promises.mkdir(proxyDir, { recursive: true });
let release: (() => Promise<void>) | undefined;
try {
release = await lockfile.lock(proxyDir, {
stale: 10000,
retries: { retries: 20, minTimeout: 50, maxTimeout: 250 },
realpath: false,
});
} catch (error) {
throw new Error(
`Failed to lock OpenAI-compatible proxy directory (${proxyDir}): ${(error as Error).message}`
);
}
try {
return await operation();
} finally {
if (release) {
try {
await release();
} catch {
// Best-effort release.
}
}
}
}
async function isPortOccupied(port: number): Promise<boolean> {
return new Promise((resolve) => {
const socket = net.createConnection({ host: '127.0.0.1', port });
const finish = (occupied: boolean) => {
socket.removeAllListeners();
socket.destroy();
resolve(occupied);
};
socket.once('connect', () => finish(true));
socket.once('error', () => finish(false));
socket.setTimeout(500, () => {
finish(false);
});
});
}
async function findOpenAICompatProxyPort(): Promise<number> {
for (
let candidate = OPENAI_COMPAT_PROXY_DEFAULT_PORT;
candidate <= OPENAI_COMPAT_PROXY_DEFAULT_PORT + 10;
candidate += 1
) {
if (!(await isPortOccupied(candidate))) {
return candidate;
}
}
return 0;
}
async function resolveDaemonEntrypoint(): Promise<string | null> {
for (const candidate of resolveOpenAICompatProxyEntrypointCandidates()) {
try {
await fs.promises.access(candidate, fs.constants.R_OK);
return candidate;
} catch {
// Try next candidate.
}
}
return null;
}
export async function isOpenAICompatProxyRunning(port: number): Promise<boolean> {
return new Promise((resolve) => {
const req = http.request(
{ hostname: '127.0.0.1', port, path: '/health', method: 'GET', timeout: 3000 },
(res) => {
let body = '';
res.setEncoding('utf8');
res.on('data', (chunk) => (body += chunk));
res.on('end', () => {
if (res.statusCode !== 200) {
resolve(false);
return;
}
try {
const payload = JSON.parse(body) as { service?: string };
resolve(payload.service === OPENAI_COMPAT_PROXY_SERVICE_NAME);
} catch {
resolve(false);
}
});
}
);
req.on('error', () => resolve(false));
req.on('timeout', () => {
req.destroy();
resolve(false);
});
req.end();
});
}
export async function getOpenAICompatProxyStatus(): Promise<OpenAICompatProxyStatus> {
const session = readOpenAICompatProxySession();
const port = session?.port ?? OPENAI_COMPAT_PROXY_DEFAULT_PORT;
const running = await isOpenAICompatProxyRunning(port);
return {
running,
pid: running ? getOpenAICompatProxyPid() || undefined : undefined,
...session,
};
}
async function stopOpenAICompatProxyUnlocked(): Promise<{ success: boolean; error?: string }> {
const pid = getOpenAICompatProxyPid();
if (!pid) {
removeOpenAICompatProxySession();
return { success: true };
}
const ownership = verifyProcessOwnership(
pid,
(commandLine) =>
commandLine.includes('--ccs-openai-proxy-daemon') &&
commandLine.includes('proxy-daemon-entry')
);
if (ownership === 'not-owned') {
removeOpenAICompatProxyPid();
return { success: true };
}
if (ownership === 'unknown') {
return {
success: false,
error: `Refusing to stop PID ${pid}: unable to verify daemon ownership`,
};
}
if (ownership === 'not-running') {
removeOpenAICompatProxyPid();
removeOpenAICompatProxySession();
return { success: true };
}
try {
process.kill(pid, 'SIGTERM');
let attempts = 0;
while (attempts < 10) {
await new Promise((resolve) => setTimeout(resolve, 500));
try {
process.kill(pid, 0);
attempts += 1;
} catch {
break;
}
}
if (attempts >= 10) {
try {
process.kill(pid, 'SIGKILL');
} catch {
// Already exited.
}
}
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err.code !== 'ESRCH') {
return { success: false, error: `Failed to stop daemon: ${err.message}` };
}
}
removeOpenAICompatProxyPid();
removeOpenAICompatProxySession();
return { success: true };
}
export async function stopOpenAICompatProxy(): Promise<{ success: boolean; error?: string }> {
return withOpenAICompatProxyLock(() => stopOpenAICompatProxyUnlocked());
}
export async function startOpenAICompatProxy(
profile: OpenAICompatProfileConfig,
options: { port?: number; insecure?: boolean } = {}
): Promise<StartOpenAICompatProxyResult> {
return withOpenAICompatProxyLock(async () => {
const status = await getOpenAICompatProxyStatus();
const port =
typeof options.port === 'number'
? options.port
: status.running && status.profileName === profile.profileName && status.port
? status.port
: await findOpenAICompatProxyPort();
if (port === 0) {
return {
success: false,
port: OPENAI_COMPAT_PROXY_DEFAULT_PORT,
error: `No free proxy port found in range ${OPENAI_COMPAT_PROXY_DEFAULT_PORT}-${OPENAI_COMPAT_PROXY_DEFAULT_PORT + 10}`,
};
}
if (!Number.isInteger(port) || port < 1 || port > 65535) {
return { success: false, port, error: `Invalid port: ${port}` };
}
if (status.running && status.profileName === profile.profileName && status.port === port) {
return {
success: true,
alreadyRunning: true,
pid: status.pid,
port,
authToken: status.authToken,
};
}
if (status.running) {
if (status.profileName !== profile.profileName) {
return {
success: false,
port,
error: `Proxy already running for profile "${status.profileName}" on port ${status.port}. Stop it before starting a different profile.`,
};
}
const stopped = await stopOpenAICompatProxyUnlocked();
if (!stopped.success) {
return {
success: false,
port,
error: stopped.error || 'Failed to restart the running proxy',
};
}
}
const daemonEntry = await resolveDaemonEntrypoint();
if (!daemonEntry) {
return {
success: false,
port,
error: 'OpenAI proxy daemon entrypoint not found. Run `bun run build` and retry.',
};
}
return new Promise((resolve) => {
let resolved = false;
let timeout: NodeJS.Timeout | null = null;
const authToken = generateProxyAuthToken();
const finish = (result: StartOpenAICompatProxyResult) => {
if (resolved) return;
resolved = true;
if (timeout) clearTimeout(timeout);
if (!result.success) {
removeOpenAICompatProxyPid();
removeOpenAICompatProxySession();
}
resolve(result);
};
const proc: ChildProcess = spawn(
process.execPath,
[
daemonEntry,
'--port',
String(port),
'--profile',
profile.profileName,
'--settings-path',
profile.settingsPath,
'--auth-token',
authToken,
...(options.insecure ? ['--insecure'] : []),
'--ccs-openai-proxy-daemon',
],
{ stdio: 'ignore', detached: true }
);
proc.unref();
if (proc.pid) {
writeOpenAICompatProxyPid(proc.pid);
}
writeOpenAICompatProxySession({
profileName: profile.profileName,
settingsPath: profile.settingsPath,
port,
baseUrl: profile.baseUrl,
authToken,
model: profile.model,
insecure: options.insecure,
});
let attempts = 0;
const poll = async () => {
attempts += 1;
if (await isOpenAICompatProxyRunning(port)) {
finish({ success: true, pid: proc.pid, port, authToken });
return;
}
if (attempts >= 30) {
finish({
success: false,
port,
error: `Proxy daemon did not start within 30 seconds on port ${port}`,
});
return;
}
timeout = setTimeout(poll, 1000);
};
timeout = setTimeout(poll, 1000);
proc.on('error', (error) => {
finish({ success: false, port, error: error.message });
});
proc.on('exit', (code, signal) => {
if (code === 0) {
finish({
success: false,
port,
error: 'Proxy daemon exited before becoming healthy',
});
return;
}
if (code !== null) {
finish({
success: false,
port,
error: `Proxy daemon exited with code ${code}`,
});
return;
}
finish({
success: false,
port,
error: `Proxy daemon was killed by signal ${signal}`,
});
});
});
});
}
+23
View File
@@ -0,0 +1,23 @@
import type { OpenAICompatProfileConfig } from './profile-router';
export function buildOpenAICompatProxyEnv(
profile: OpenAICompatProfileConfig,
port: number,
authToken: string,
claudeConfigDir?: string
): Record<string, string> {
return {
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
ANTHROPIC_AUTH_TOKEN: authToken,
...(profile.model ? { ANTHROPIC_MODEL: profile.model } : {}),
...(profile.opusModel ? { ANTHROPIC_DEFAULT_OPUS_MODEL: profile.opusModel } : {}),
...(profile.sonnetModel ? { ANTHROPIC_DEFAULT_SONNET_MODEL: profile.sonnetModel } : {}),
...(profile.haikuModel
? {
ANTHROPIC_DEFAULT_HAIKU_MODEL: profile.haikuModel,
ANTHROPIC_SMALL_FAST_MODEL: profile.haikuModel,
}
: {}),
...(claudeConfigDir ? { CLAUDE_CONFIG_DIR: claudeConfigDir } : {}),
};
}
+81
View File
@@ -0,0 +1,81 @@
import * as http from 'http';
import { Readable } from 'stream';
const MAX_BODY_SIZE = 10 * 1024 * 1024;
export function writeJson(res: http.ServerResponse, statusCode: number, payload: unknown): void {
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(payload));
}
export function readJsonBody(req: http.IncomingMessage): Promise<unknown> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
let total = 0;
let settled = false;
const resolveOnce = (payload: unknown) => {
if (!settled) {
settled = true;
resolve(payload);
}
};
const rejectOnce = (error: Error) => {
if (!settled) {
settled = true;
reject(error);
}
};
req.on('data', (chunk: Buffer) => {
total += chunk.length;
if (total > MAX_BODY_SIZE) {
req.pause();
rejectOnce(new Error('Request body too large (max 10MB)'));
return;
}
chunks.push(chunk);
});
req.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf8').trim();
if (!raw) {
resolveOnce({});
return;
}
try {
resolveOnce(JSON.parse(raw));
} catch {
rejectOnce(new Error('Invalid JSON in request body'));
}
});
req.on('error', (error) => {
rejectOnce(error instanceof Error ? error : new Error(String(error)));
});
});
}
export async function pipeWebResponseToNode(
response: Response,
res: http.ServerResponse
): Promise<void> {
res.statusCode = response.status;
response.headers.forEach((value, key) => {
res.setHeader(key, value);
});
if (!response.body) {
res.end();
return;
}
const nodeStream = Readable.fromWeb(response.body as unknown as ReadableStream<Uint8Array>);
await new Promise<void>((resolve, reject) => {
nodeStream.on('error', reject);
nodeStream.on('end', resolve);
nodeStream.pipe(res);
});
}
+174
View File
@@ -0,0 +1,174 @@
import * as http from 'http';
import type { Dispatcher } from 'undici';
import type { OpenAICompatProfileConfig } from '../profile-router';
import { ProxyRequestTransformer } from '../transformers/request-transformer';
import { ProxySseStreamTransformer } from '../transformers/sse-stream-transformer';
import { resolveOpenAIChatCompletionsUrl } from '../upstream-url';
import { pipeWebResponseToNode, readJsonBody, writeJson } from './http-helpers';
const REQUEST_TIMEOUT_MS = 30_000;
class ProxyInputError extends Error {
constructor(message: string) {
super(message);
this.name = 'ProxyInputError';
}
}
function buildUpstreamHeaders(profile: OpenAICompatProfileConfig): Record<string, string> {
return {
'Content-Type': 'application/json',
Authorization: `Bearer ${profile.apiKey}`,
'User-Agent': 'CCS-OpenAI-Compat-Proxy/1.0',
};
}
function buildUpstreamBody(profile: OpenAICompatProfileConfig, rawBody: unknown): string {
let transformed;
try {
const transformer = new ProxyRequestTransformer();
transformed = transformer.transform(rawBody);
} catch (error) {
const message = error instanceof Error ? error.message : 'Invalid Anthropic request';
throw new ProxyInputError(message);
}
const body = {
...transformed,
model: transformed.model || profile.model,
stream: transformed.stream === true,
};
return JSON.stringify(body);
}
export function extractIncomingProxyToken(headers: http.IncomingHttpHeaders): string | null {
const xApiKey = headers['x-api-key'];
if (typeof xApiKey === 'string' && xApiKey.trim().length > 0) {
return xApiKey.trim();
}
const anthropicApiKey = headers['anthropic-api-key'];
if (typeof anthropicApiKey === 'string' && anthropicApiKey.trim().length > 0) {
return anthropicApiKey.trim();
}
const authHeader = headers.authorization;
if (typeof authHeader === 'string' && authHeader.trim().length > 0) {
const trimmed = authHeader.trim();
const bearerPrefix = 'Bearer ';
return trimmed.startsWith(bearerPrefix) ? trimmed.slice(bearerPrefix.length).trim() : trimmed;
}
return null;
}
export function validateIncomingProxyAuth(
headers: http.IncomingHttpHeaders,
expectedToken: string
): boolean {
return extractIncomingProxyToken(headers) === expectedToken;
}
function buildFetchInit(
profile: OpenAICompatProfileConfig,
body: string,
signal: AbortSignal,
insecureDispatcher?: Dispatcher
): RequestInit {
const init: RequestInit = {
method: 'POST',
headers: buildUpstreamHeaders(profile),
body,
signal,
};
if (insecureDispatcher) {
(init as Record<string, unknown>).dispatcher = insecureDispatcher;
}
return init;
}
export async function handleProxyMessagesRequest(
req: http.IncomingMessage,
res: http.ServerResponse,
profile: OpenAICompatProfileConfig,
expectedAuthToken: string,
insecureDispatcher?: Dispatcher
): Promise<void> {
const transformer = new ProxySseStreamTransformer();
if (!validateIncomingProxyAuth(req.headers, expectedAuthToken)) {
await pipeWebResponseToNode(
transformer.error(401, 'authentication_error', 'Missing or invalid local proxy token'),
res
);
return;
}
try {
const rawBody = await readJsonBody(req);
const upstreamBody = buildUpstreamBody(profile, rawBody);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
const abortOnDisconnect = () => {
if (!controller.signal.aborted && !res.writableEnded) {
controller.abort();
}
};
req.on('aborted', abortOnDisconnect);
req.on('close', abortOnDisconnect);
res.on('close', abortOnDisconnect);
try {
const upstreamResponse = await fetch(
resolveOpenAIChatCompletionsUrl(profile.baseUrl),
buildFetchInit(profile, upstreamBody, controller.signal, insecureDispatcher)
);
clearTimeout(timeout);
const response = await transformer.transform(upstreamResponse);
await pipeWebResponseToNode(response, res);
} finally {
clearTimeout(timeout);
}
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown proxy error';
const status =
error instanceof Error && error.name === 'AbortError'
? 502
: error instanceof ProxyInputError
? 400
: message.includes('Request body too large')
? 413
: message.includes('Invalid JSON')
? 400
: 502;
const type = status >= 500 ? 'api_error' : 'invalid_request_error';
await pipeWebResponseToNode(
transformer.error(
status,
type,
error instanceof Error && error.name === 'AbortError'
? 'The upstream provider did not respond within 30 seconds'
: message
),
res
);
}
}
export function handleProxyModelsRequest(
res: http.ServerResponse,
profile: OpenAICompatProfileConfig
): void {
const data = [profile.model, profile.opusModel, profile.sonnetModel, profile.haikuModel]
.filter((value): value is string => typeof value === 'string' && value.length > 0)
.map((id) => ({
id,
object: 'model',
created: 0,
owned_by: profile.provider,
}));
writeJson(res, 200, { object: 'list', data });
}
+75
View File
@@ -0,0 +1,75 @@
import * as http from 'http';
import { Agent } from 'undici';
import type { OpenAICompatProfileConfig } from '../profile-router';
import { OPENAI_COMPAT_PROXY_SERVICE_NAME } from '../proxy-daemon-paths';
import {
handleProxyMessagesRequest,
handleProxyModelsRequest,
validateIncomingProxyAuth,
} from './messages-route';
import { writeJson } from './http-helpers';
export interface OpenAICompatProxyServerOptions {
profile: OpenAICompatProfileConfig;
port: number;
authToken: string;
insecure?: boolean;
}
export function startOpenAICompatProxyServer(options: OpenAICompatProxyServerOptions): http.Server {
const insecureDispatcher = options.insecure
? new Agent({ connect: { rejectUnauthorized: false } })
: undefined;
const server = http.createServer(async (req, res) => {
const method = req.method || 'GET';
const requestUrl = req.url || '/';
const parsedUrl = new URL(requestUrl, 'http://127.0.0.1');
const pathname =
parsedUrl.pathname.length > 1 ? parsedUrl.pathname.replace(/\/+$/, '') : parsedUrl.pathname;
if (method === 'GET' && pathname === '/health') {
writeJson(res, 200, {
ok: true,
service: OPENAI_COMPAT_PROXY_SERVICE_NAME,
profile: options.profile.profileName,
port: options.port,
});
return;
}
if (method === 'GET' && pathname === '/v1/models') {
if (!validateIncomingProxyAuth(req.headers, options.authToken)) {
writeJson(res, 401, {
type: 'error',
error: {
type: 'authentication_error',
message: 'Missing or invalid local proxy token',
},
});
return;
}
handleProxyModelsRequest(res, options.profile);
return;
}
if (method === 'POST' && pathname === '/v1/messages') {
await handleProxyMessagesRequest(
req,
res,
options.profile,
options.authToken,
insecureDispatcher
);
return;
}
writeJson(res, 404, { error: 'Not found' });
});
server.on('close', () => {
void insecureDispatcher?.close();
});
server.listen(options.port, '127.0.0.1');
return server;
}
@@ -0,0 +1,89 @@
import { translateAnthropicRequest } from '../../cursor/cursor-anthropic-translator';
interface AnthropicProxyRequestShape {
max_tokens?: unknown;
temperature?: unknown;
top_p?: unknown;
stop_sequences?: unknown;
metadata?: unknown;
tools?: unknown;
}
export interface ProxyOpenAIRequest extends ReturnType<typeof translateAnthropicRequest> {
max_tokens?: number;
temperature?: number;
top_p?: number;
stop?: string[];
metadata?: Record<string, unknown>;
tools?: Array<{
type: 'function';
function: {
name: string;
description?: string;
parameters: Record<string, unknown>;
};
}>;
}
function asNumber(value: unknown): number | undefined {
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
}
function asStringArray(value: unknown): string[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
const result = value.filter(
(entry): entry is string => typeof entry === 'string' && entry.length > 0
);
return result.length > 0 ? result : undefined;
}
function asMetadata(value: unknown): Record<string, unknown> | undefined {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}
function transformTools(value: unknown): ProxyOpenAIRequest['tools'] {
if (!Array.isArray(value)) {
return undefined;
}
const tools = value
.filter(
(entry): entry is { name?: unknown; description?: unknown; input_schema?: unknown } =>
typeof entry === 'object' && entry !== null
)
.map((entry) => ({
type: 'function' as const,
function: {
name: typeof entry.name === 'string' ? entry.name : 'tool',
...(typeof entry.description === 'string' ? { description: entry.description } : {}),
parameters:
typeof entry.input_schema === 'object' && entry.input_schema !== null
? (entry.input_schema as Record<string, unknown>)
: { type: 'object', properties: {} },
},
}));
return tools.length > 0 ? tools : undefined;
}
export class ProxyRequestTransformer {
transform(raw: unknown): ProxyOpenAIRequest {
const translated = translateAnthropicRequest(raw);
const source = (raw || {}) as AnthropicProxyRequestShape;
return {
...translated,
max_tokens: asNumber(source.max_tokens),
temperature: asNumber(source.temperature),
top_p: asNumber(source.top_p),
stop: asStringArray(source.stop_sequences),
metadata: asMetadata(source.metadata),
tools: transformTools(source.tools),
};
}
}
@@ -0,0 +1,14 @@
import {
createAnthropicErrorResponse,
createAnthropicProxyResponse,
} from '../../cursor/cursor-anthropic-response';
export class ProxySseStreamTransformer {
async transform(response: Response): Promise<Response> {
return createAnthropicProxyResponse(response);
}
error(status: number, type: string, message: string): Response {
return createAnthropicErrorResponse(status, type, message);
}
}
+36
View File
@@ -0,0 +1,36 @@
function normalizePathname(pathname: string): string {
const trimmed = pathname.replace(/\/+$/, '');
return trimmed || '';
}
function ensureSupportedProtocol(parsed: URL): void {
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
throw new Error(`Unsupported upstream protocol: ${parsed.protocol}`);
}
}
function buildResolvedUrl(baseUrl: string, suffix: string): string {
const parsed = new URL(baseUrl);
ensureSupportedProtocol(parsed);
const pathname = normalizePathname(parsed.pathname);
if (pathname.endsWith(suffix)) {
return parsed.toString();
}
if (pathname.endsWith('/v1') || pathname.endsWith('/api')) {
parsed.pathname = `${pathname}${suffix.startsWith('/') ? suffix : `/${suffix}`}`;
return parsed.toString();
}
parsed.pathname = pathname ? `${pathname}/v1${suffix}` : `/v1${suffix}`;
return parsed.toString();
}
export function resolveOpenAIChatCompletionsUrl(baseUrl: string): string {
return buildResolvedUrl(baseUrl, '/chat/completions');
}
export function resolveOpenAIModelsUrl(baseUrl: string): string {
return buildResolvedUrl(baseUrl, '/models');
}