codex-claude-proxy 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/docs/ACCOUNTS.md +202 -0
- package/docs/API.md +274 -0
- package/docs/ARCHITECTURE.md +133 -0
- package/docs/CLAUDE_INTEGRATION.md +163 -0
- package/docs/OAUTH.md +201 -0
- package/docs/OPENCLAW.md +338 -0
- package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
- package/package.json +44 -0
- package/public/css/style.css +791 -0
- package/public/index.html +783 -0
- package/public/js/app.js +511 -0
- package/src/account-manager.js +483 -0
- package/src/claude-config.js +143 -0
- package/src/cli/accounts.js +413 -0
- package/src/cli/index.js +66 -0
- package/src/direct-api.js +123 -0
- package/src/format-converter.js +331 -0
- package/src/index.js +41 -0
- package/src/kilo-api.js +68 -0
- package/src/kilo-format-converter.js +270 -0
- package/src/kilo-streamer.js +198 -0
- package/src/model-api.js +189 -0
- package/src/oauth.js +554 -0
- package/src/response-streamer.js +329 -0
- package/src/routes/api-routes.js +1035 -0
- package/src/server-settings.js +48 -0
- package/src/server.js +30 -0
- package/src/utils/logger.js +156 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createInterface } from 'readline/promises';
|
|
4
|
+
import { stdin, stdout } from 'process';
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
6
|
+
import { dirname } from 'path';
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import net from 'net';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import crypto from 'crypto';
|
|
12
|
+
|
|
13
|
+
const CONFIG_DIR = join(homedir(), '.codex-claude-proxy');
|
|
14
|
+
const ACCOUNTS_FILE = join(CONFIG_DIR, 'accounts.json');
|
|
15
|
+
const DEFAULT_PORT = 8081;
|
|
16
|
+
|
|
17
|
+
const OAUTH_CONFIG = {
|
|
18
|
+
clientId: 'app_EMoamEEZ73f0CkXaXp7hrann',
|
|
19
|
+
authUrl: 'https://auth.openai.com/oauth/authorize',
|
|
20
|
+
tokenUrl: 'https://auth.openai.com/oauth/token',
|
|
21
|
+
scopes: ['openid', 'profile', 'email', 'offline_access'],
|
|
22
|
+
callbackPort: 1455,
|
|
23
|
+
callbackPath: '/auth/callback'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function loadAccounts() {
|
|
27
|
+
try {
|
|
28
|
+
if (existsSync(ACCOUNTS_FILE)) {
|
|
29
|
+
const data = readFileSync(ACCOUNTS_FILE, 'utf-8');
|
|
30
|
+
return JSON.parse(data);
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Error loading accounts:', error.message);
|
|
34
|
+
}
|
|
35
|
+
return { accounts: [], activeAccount: null, version: 1 };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function saveAccounts(data) {
|
|
39
|
+
try {
|
|
40
|
+
const dir = dirname(ACCOUNTS_FILE);
|
|
41
|
+
if (!existsSync(dir)) {
|
|
42
|
+
mkdirSync(dir, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
writeFileSync(ACCOUNTS_FILE, JSON.stringify(data, null, 2));
|
|
45
|
+
console.log(`\n✓ Saved ${data.accounts.length} account(s) to ${ACCOUNTS_FILE}`);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Error saving accounts:', error.message);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function displayAccounts(data) {
|
|
53
|
+
if (!data.accounts || data.accounts.length === 0) {
|
|
54
|
+
console.log('\nNo accounts configured.');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(`\n${data.accounts.length} account(s) saved:`);
|
|
59
|
+
data.accounts.forEach((acc, i) => {
|
|
60
|
+
const active = acc.email === data.activeAccount ? ' (ACTIVE)' : '';
|
|
61
|
+
console.log(` ${i + 1}. ${acc.email}${active}`);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function generatePKCE() {
|
|
66
|
+
const verifier = crypto.randomBytes(32).toString('base64url');
|
|
67
|
+
const challenge = crypto
|
|
68
|
+
.createHash('sha256')
|
|
69
|
+
.update(verifier)
|
|
70
|
+
.digest('base64url');
|
|
71
|
+
return { verifier, challenge };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function generateState() {
|
|
75
|
+
return crypto.randomBytes(16).toString('hex');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getAuthorizationUrl(verifier, state, port) {
|
|
79
|
+
const challenge = crypto
|
|
80
|
+
.createHash('sha256')
|
|
81
|
+
.update(verifier)
|
|
82
|
+
.digest('base64url');
|
|
83
|
+
|
|
84
|
+
const redirectUri = `http://localhost:${port}${OAUTH_CONFIG.callbackPath}`;
|
|
85
|
+
|
|
86
|
+
const params = new URLSearchParams({
|
|
87
|
+
response_type: 'code',
|
|
88
|
+
client_id: OAUTH_CONFIG.clientId,
|
|
89
|
+
redirect_uri: redirectUri,
|
|
90
|
+
scope: OAUTH_CONFIG.scopes.join(' '),
|
|
91
|
+
code_challenge: challenge,
|
|
92
|
+
code_challenge_method: 'S256',
|
|
93
|
+
state: state,
|
|
94
|
+
prompt: 'login',
|
|
95
|
+
max_age: '0'
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return `${OAUTH_CONFIG.authUrl}?${params.toString()}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function decodeJWT(token) {
|
|
102
|
+
try {
|
|
103
|
+
const parts = token.split('.');
|
|
104
|
+
if (parts.length !== 3) return null;
|
|
105
|
+
const payload = Buffer.from(parts[1], 'base64').toString('utf8');
|
|
106
|
+
return JSON.parse(payload);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function extractAccountInfo(accessToken) {
|
|
113
|
+
const payload = decodeJWT(accessToken);
|
|
114
|
+
if (!payload) return null;
|
|
115
|
+
|
|
116
|
+
const authInfo = payload['https://api.openai.com/auth'] || {};
|
|
117
|
+
const profileInfo = payload['https://api.openai.com/profile'] || {};
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
accountId: authInfo.chatgpt_account_id || null,
|
|
121
|
+
planType: authInfo.chatgpt_plan_type || 'free',
|
|
122
|
+
userId: authInfo.chatgpt_user_id || payload.sub || null,
|
|
123
|
+
email: profileInfo.email || payload.email || null,
|
|
124
|
+
expiresAt: payload.exp ? payload.exp * 1000 : null
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function exchangeCodeForTokens(code, verifier, port) {
|
|
129
|
+
const redirectUri = `http://localhost:${port}${OAUTH_CONFIG.callbackPath}`;
|
|
130
|
+
|
|
131
|
+
const response = await fetch(OAUTH_CONFIG.tokenUrl, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: {
|
|
134
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
135
|
+
},
|
|
136
|
+
body: new URLSearchParams({
|
|
137
|
+
grant_type: 'authorization_code',
|
|
138
|
+
code: code,
|
|
139
|
+
redirect_uri: redirectUri,
|
|
140
|
+
client_id: OAUTH_CONFIG.clientId,
|
|
141
|
+
code_verifier: verifier
|
|
142
|
+
})
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
const error = await response.text();
|
|
147
|
+
throw new Error(`Token exchange failed: ${response.status} - ${error}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const tokens = await response.json();
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
accessToken: tokens.access_token,
|
|
154
|
+
refreshToken: tokens.refresh_token,
|
|
155
|
+
idToken: tokens.id_token,
|
|
156
|
+
expiresIn: tokens.expires_in
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function extractCodeFromInput(input) {
|
|
161
|
+
if (!input || typeof input !== 'string') {
|
|
162
|
+
throw new Error('No input provided');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const trimmed = input.trim();
|
|
166
|
+
|
|
167
|
+
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
|
168
|
+
try {
|
|
169
|
+
const url = new URL(trimmed);
|
|
170
|
+
const code = url.searchParams.get('code');
|
|
171
|
+
const error = url.searchParams.get('error');
|
|
172
|
+
|
|
173
|
+
if (error) {
|
|
174
|
+
throw new Error(`OAuth error: ${error}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!code) {
|
|
178
|
+
throw new Error('No authorization code found in URL');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { code };
|
|
182
|
+
} catch (e) {
|
|
183
|
+
if (e.message.includes('OAuth error') || e.message.includes('No authorization code')) {
|
|
184
|
+
throw e;
|
|
185
|
+
}
|
|
186
|
+
throw new Error('Invalid URL format');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (trimmed.length < 10) {
|
|
191
|
+
throw new Error('Input is too short to be a valid authorization code');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { code: trimmed };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function openBrowser(url) {
|
|
198
|
+
const platform = process.platform;
|
|
199
|
+
let command;
|
|
200
|
+
let args;
|
|
201
|
+
|
|
202
|
+
if (platform === 'darwin') {
|
|
203
|
+
command = 'open';
|
|
204
|
+
args = [url];
|
|
205
|
+
} else if (platform === 'win32') {
|
|
206
|
+
command = 'cmd';
|
|
207
|
+
args = ['/c', 'start', '', url.replace(/&/g, '^&')];
|
|
208
|
+
} else {
|
|
209
|
+
command = 'xdg-open';
|
|
210
|
+
args = [url];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const child = spawn(command, args, { stdio: 'ignore', detached: true });
|
|
214
|
+
child.on('error', () => {
|
|
215
|
+
console.log('\n⚠ Could not open browser automatically.');
|
|
216
|
+
console.log('Please open this URL manually:', url);
|
|
217
|
+
});
|
|
218
|
+
child.unref();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function isServerRunning(port) {
|
|
222
|
+
return new Promise((resolve) => {
|
|
223
|
+
const socket = new net.Socket();
|
|
224
|
+
socket.setTimeout(1000);
|
|
225
|
+
|
|
226
|
+
socket.on('connect', () => {
|
|
227
|
+
socket.destroy();
|
|
228
|
+
resolve(true);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
socket.on('timeout', () => {
|
|
232
|
+
socket.destroy();
|
|
233
|
+
resolve(false);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
socket.on('error', () => {
|
|
237
|
+
socket.destroy();
|
|
238
|
+
resolve(false);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
socket.connect(port, 'localhost');
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function ensureServerStopped(port) {
|
|
246
|
+
const isRunning = await isServerRunning(port);
|
|
247
|
+
if (isRunning) {
|
|
248
|
+
console.error(`
|
|
249
|
+
\x1b[31mError: Proxy server is currently running on port ${port}.\x1b[0m
|
|
250
|
+
|
|
251
|
+
Please stop the server (Ctrl+C) before adding or managing accounts.
|
|
252
|
+
This ensures that your account changes are loaded correctly.
|
|
253
|
+
`);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function createRL() {
|
|
259
|
+
return createInterface({ input: stdin, output: stdout });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function addAccountManual(rl) {
|
|
263
|
+
console.log('\n=== Add ChatGPT Account (No-Browser Mode) ===\n');
|
|
264
|
+
|
|
265
|
+
const { verifier } = generatePKCE();
|
|
266
|
+
const state = generateState();
|
|
267
|
+
const url = getAuthorizationUrl(verifier, state, OAUTH_CONFIG.callbackPort);
|
|
268
|
+
|
|
269
|
+
console.log('Copy the following URL and open it in a browser on another device:\n');
|
|
270
|
+
console.log(` ${url}\n`);
|
|
271
|
+
console.log('After signing in, you will be redirected to a localhost URL.');
|
|
272
|
+
console.log('Copy the ENTIRE redirect URL or just the authorization code.\n');
|
|
273
|
+
|
|
274
|
+
const input = await rl.question('Paste the callback URL or authorization code: ');
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const { code } = extractCodeFromInput(input);
|
|
278
|
+
|
|
279
|
+
console.log('\nExchanging authorization code for tokens...');
|
|
280
|
+
const tokens = await exchangeCodeForTokens(code, verifier, OAUTH_CONFIG.callbackPort);
|
|
281
|
+
const accountInfo = extractAccountInfo(tokens.accessToken);
|
|
282
|
+
|
|
283
|
+
const data = loadAccounts();
|
|
284
|
+
|
|
285
|
+
const existingIndex = data.accounts.findIndex(a => a.email === accountInfo?.email);
|
|
286
|
+
const newAccount = {
|
|
287
|
+
email: accountInfo?.email || 'unknown',
|
|
288
|
+
accountId: accountInfo?.accountId,
|
|
289
|
+
planType: accountInfo?.planType || 'free',
|
|
290
|
+
accessToken: tokens.accessToken,
|
|
291
|
+
refreshToken: tokens.refreshToken,
|
|
292
|
+
idToken: tokens.idToken,
|
|
293
|
+
expiresAt: accountInfo?.expiresAt || (Date.now() + tokens.expiresIn * 1000),
|
|
294
|
+
addedAt: new Date().toISOString(),
|
|
295
|
+
lastUsed: null
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
if (existingIndex >= 0) {
|
|
299
|
+
data.accounts[existingIndex] = newAccount;
|
|
300
|
+
console.log(`\n⚠ Account ${newAccount.email} already exists. Updating tokens.`);
|
|
301
|
+
} else {
|
|
302
|
+
data.accounts.push(newAccount);
|
|
303
|
+
if (!data.activeAccount) {
|
|
304
|
+
data.activeAccount = newAccount.email;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
saveAccounts(data);
|
|
309
|
+
console.log(`\n✓ Successfully authenticated: ${newAccount.email}`);
|
|
310
|
+
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.error(`\n✗ Authentication failed: ${error.message}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function addAccountBrowser() {
|
|
317
|
+
console.log('\n=== Add ChatGPT Account ===\n');
|
|
318
|
+
|
|
319
|
+
const { verifier } = generatePKCE();
|
|
320
|
+
const state = generateState();
|
|
321
|
+
const url = getAuthorizationUrl(verifier, state, OAUTH_CONFIG.callbackPort);
|
|
322
|
+
|
|
323
|
+
console.log('Opening browser for ChatGPT sign-in...');
|
|
324
|
+
console.log('(If browser does not open, copy this URL manually)\n');
|
|
325
|
+
console.log(` ${url}\n`);
|
|
326
|
+
|
|
327
|
+
openBrowser(url);
|
|
328
|
+
|
|
329
|
+
console.log('After authorization, complete the flow via:');
|
|
330
|
+
console.log(` curl -X POST http://localhost:${DEFAULT_PORT}/accounts/add/manual \\`);
|
|
331
|
+
console.log(` -H "Content-Type: application/json" \\`);
|
|
332
|
+
console.log(` -d '{"code":"<auth_code>","verifier":"${verifier}"}'`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function listAccounts() {
|
|
336
|
+
const data = loadAccounts();
|
|
337
|
+
displayAccounts(data);
|
|
338
|
+
if (data.accounts.length > 0) {
|
|
339
|
+
console.log(`\nConfig file: ${ACCOUNTS_FILE}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function clearAccounts(rl) {
|
|
344
|
+
const data = loadAccounts();
|
|
345
|
+
|
|
346
|
+
if (!data.accounts || data.accounts.length === 0) {
|
|
347
|
+
console.log('No accounts to clear.');
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
displayAccounts(data);
|
|
352
|
+
|
|
353
|
+
const confirm = await rl.question('\nAre you sure you want to remove all accounts? [y/N]: ');
|
|
354
|
+
if (confirm.toLowerCase() === 'y') {
|
|
355
|
+
data.accounts = [];
|
|
356
|
+
data.activeAccount = null;
|
|
357
|
+
saveAccounts(data);
|
|
358
|
+
console.log('All accounts removed.');
|
|
359
|
+
} else {
|
|
360
|
+
console.log('Cancelled.');
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function main() {
|
|
365
|
+
const args = process.argv.slice(2);
|
|
366
|
+
const command = args[0] || 'help';
|
|
367
|
+
const noBrowser = args.includes('--no-browser');
|
|
368
|
+
const port = parseInt(args.find(a => a.startsWith('--port='))?.split('=')[1]) || DEFAULT_PORT;
|
|
369
|
+
|
|
370
|
+
console.log('╔════════════════════════════════════════╗');
|
|
371
|
+
console.log('║ Codex Proxy Account Manager ║');
|
|
372
|
+
console.log('║ Use --no-browser for headless mode ║');
|
|
373
|
+
console.log('╚════════════════════════════════════════╝');
|
|
374
|
+
|
|
375
|
+
const rl = createRL();
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
switch (command) {
|
|
379
|
+
case 'add':
|
|
380
|
+
if (noBrowser) {
|
|
381
|
+
await addAccountManual(rl);
|
|
382
|
+
} else {
|
|
383
|
+
await ensureServerStopped(port);
|
|
384
|
+
await addAccountBrowser();
|
|
385
|
+
}
|
|
386
|
+
break;
|
|
387
|
+
case 'list':
|
|
388
|
+
await listAccounts();
|
|
389
|
+
break;
|
|
390
|
+
case 'clear':
|
|
391
|
+
await ensureServerStopped(port);
|
|
392
|
+
await clearAccounts(rl);
|
|
393
|
+
break;
|
|
394
|
+
case 'help':
|
|
395
|
+
default:
|
|
396
|
+
console.log('\nUsage:');
|
|
397
|
+
console.log(' node src/cli/accounts.js add Add account (opens browser)');
|
|
398
|
+
console.log(' node src/cli/accounts.js add --no-browser Add account (manual code)');
|
|
399
|
+
console.log(' node src/cli/accounts.js list List all accounts');
|
|
400
|
+
console.log(' node src/cli/accounts.js clear Remove all accounts');
|
|
401
|
+
console.log(' node src/cli/accounts.js help Show this help');
|
|
402
|
+
console.log('\nOptions:');
|
|
403
|
+
console.log(' --no-browser Manual authorization code input (for headless servers)');
|
|
404
|
+
console.log(' --port=<port> Server port (default: 8081)');
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
} finally {
|
|
408
|
+
rl.close();
|
|
409
|
+
process.exit(0);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
main().catch(console.error);
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* codex-claude-proxy CLI
|
|
5
|
+
*
|
|
6
|
+
* Antigravity-style UX:
|
|
7
|
+
* codex-claude-proxy start
|
|
8
|
+
* codex-claude-proxy accounts <subcommand>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn } from 'child_process';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import { dirname, join } from 'path';
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
|
|
17
|
+
function printHelp() {
|
|
18
|
+
console.log(`\nCodex Claude Proxy\n\nUsage:\n codex-claude-proxy start [--port <port>]\n codex-claude-proxy accounts <add|list|...> [args]\n\nNotes:\n - \'start\' runs the proxy server\n - \'accounts\' delegates to the accounts CLI\n`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function runNodeScript(scriptPath, args) {
|
|
22
|
+
const child = spawn(process.execPath, [scriptPath, ...args], {
|
|
23
|
+
stdio: 'inherit',
|
|
24
|
+
env: process.env
|
|
25
|
+
});
|
|
26
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const argv = process.argv.slice(2);
|
|
30
|
+
const cmd = argv[0];
|
|
31
|
+
|
|
32
|
+
if (!cmd || cmd === '-h' || cmd === '--help' || cmd === 'help') {
|
|
33
|
+
printHelp();
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (cmd === 'start') {
|
|
38
|
+
// Pass through args like --port to the server via env or args.
|
|
39
|
+
// Current server reads PORT from env, so we translate --port into PORT.
|
|
40
|
+
const args = argv.slice(1);
|
|
41
|
+
|
|
42
|
+
const portFlagIndex = args.findIndex((a) => a === '--port' || a === '-p');
|
|
43
|
+
if (portFlagIndex !== -1) {
|
|
44
|
+
const portValue = args[portFlagIndex + 1];
|
|
45
|
+
if (!portValue || String(Number(portValue)) !== portValue) {
|
|
46
|
+
console.error('Invalid port. Usage: codex-claude-proxy start --port <port>');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
process.env.PORT = portValue;
|
|
50
|
+
args.splice(portFlagIndex, 2);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const serverEntrypoint = join(__dirname, '..', 'index.js');
|
|
54
|
+
runNodeScript(serverEntrypoint, args);
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (cmd === 'accounts') {
|
|
59
|
+
const accountsEntrypoint = join(__dirname, 'accounts.js');
|
|
60
|
+
runNodeScript(accountsEntrypoint, argv.slice(1));
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.error(`Unknown command: ${cmd}`);
|
|
65
|
+
printHelp();
|
|
66
|
+
process.exit(1);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct API Client
|
|
3
|
+
* Makes direct HTTP calls to ChatGPT's backend API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { convertAnthropicToResponsesAPI, convertOutputToAnthropic, generateMessageId } from './format-converter.js';
|
|
7
|
+
import { streamResponsesAPI, parseResponsesAPIResponse } from './response-streamer.js';
|
|
8
|
+
|
|
9
|
+
const API_URL = 'https://chatgpt.com/backend-api/codex/responses';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Send a streaming request to ChatGPT API
|
|
13
|
+
*/
|
|
14
|
+
export async function* sendMessageStream(anthropicRequest, accessToken, accountId) {
|
|
15
|
+
const request = convertAnthropicToResponsesAPI(anthropicRequest);
|
|
16
|
+
|
|
17
|
+
const response = await fetch(API_URL, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: {
|
|
20
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
21
|
+
'ChatGPT-Account-ID': accountId,
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
'Accept': 'text/event-stream'
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify(request)
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
const errorText = await response.text();
|
|
30
|
+
|
|
31
|
+
if (response.status === 401) {
|
|
32
|
+
throw new Error('AUTH_EXPIRED: Token expired or revoked. Please re-authenticate.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (response.status === 429) {
|
|
36
|
+
throw new Error(`RATE_LIMITED: ${errorText}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (response.status === 403) {
|
|
40
|
+
if (errorText.includes('challenge') || errorText.includes('cloudflare')) {
|
|
41
|
+
throw new Error('CLOUDFLARE_BLOCKED: Request blocked by Cloudflare.');
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`FORBIDDEN: ${errorText}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (response.status === 400) {
|
|
47
|
+
throw new Error(`INVALID_REQUEST: ${errorText}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
throw new Error(`API_ERROR: ${response.status} - ${errorText}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
yield* streamResponsesAPI(response, anthropicRequest.model);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Send a non-streaming request to ChatGPT API
|
|
58
|
+
*/
|
|
59
|
+
export async function sendMessage(anthropicRequest, accessToken, accountId) {
|
|
60
|
+
const request = convertAnthropicToResponsesAPI({
|
|
61
|
+
...anthropicRequest,
|
|
62
|
+
stream: false
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const response = await fetch(API_URL, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: {
|
|
68
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
69
|
+
'ChatGPT-Account-ID': accountId,
|
|
70
|
+
'Content-Type': 'application/json',
|
|
71
|
+
'Accept': 'text/event-stream'
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify(request)
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const errorText = await response.text();
|
|
78
|
+
|
|
79
|
+
if (response.status === 401) {
|
|
80
|
+
throw new Error('AUTH_EXPIRED: Token expired or revoked. Please re-authenticate.');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
throw new Error(`API_ERROR: ${response.status} - ${errorText}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const apiResponse = await parseResponsesAPIResponse(response);
|
|
87
|
+
|
|
88
|
+
if (!apiResponse) {
|
|
89
|
+
return {
|
|
90
|
+
id: generateMessageId(),
|
|
91
|
+
type: 'message',
|
|
92
|
+
role: 'assistant',
|
|
93
|
+
content: [{ type: 'text', text: '' }],
|
|
94
|
+
model: anthropicRequest.model,
|
|
95
|
+
stop_reason: 'end_turn',
|
|
96
|
+
stop_sequence: null,
|
|
97
|
+
usage: { input_tokens: 0, output_tokens: 0 }
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const content = convertOutputToAnthropic(apiResponse.output);
|
|
102
|
+
const stopReason = content.some(c => c.type === 'tool_use') ? 'tool_use' : 'end_turn';
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
id: generateMessageId(),
|
|
106
|
+
type: 'message',
|
|
107
|
+
role: 'assistant',
|
|
108
|
+
content: content,
|
|
109
|
+
model: anthropicRequest.model,
|
|
110
|
+
stop_reason: stopReason,
|
|
111
|
+
stop_sequence: null,
|
|
112
|
+
usage: {
|
|
113
|
+
input_tokens: apiResponse.usage?.input_tokens || 0,
|
|
114
|
+
output_tokens: apiResponse.usage?.output_tokens || 0,
|
|
115
|
+
cache_read_input_tokens: apiResponse.usage?.cache_read_input_tokens || 0
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default {
|
|
121
|
+
sendMessageStream,
|
|
122
|
+
sendMessage
|
|
123
|
+
};
|