@pikoloo/codex-proxy 1.0.6

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +199 -0
  3. package/bin/cli.js +118 -0
  4. package/docs/ACCOUNTS.md +202 -0
  5. package/docs/API.md +289 -0
  6. package/docs/ARCHITECTURE.md +129 -0
  7. package/docs/CLAUDE_INTEGRATION.md +163 -0
  8. package/docs/OAUTH.md +85 -0
  9. package/docs/OPENCLAW.md +34 -0
  10. package/docs/legal.md +11 -0
  11. package/images/dashboard-screenshot.png +0 -0
  12. package/images/demo-screenshot.png +0 -0
  13. package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
  14. package/package.json +61 -0
  15. package/public/css/style.css +1502 -0
  16. package/public/index.html +827 -0
  17. package/public/js/app.js +601 -0
  18. package/src/account-manager.js +528 -0
  19. package/src/account-rotation/index.js +93 -0
  20. package/src/account-rotation/rate-limits.js +293 -0
  21. package/src/account-rotation/strategies/base-strategy.js +48 -0
  22. package/src/account-rotation/strategies/index.js +31 -0
  23. package/src/account-rotation/strategies/round-robin-strategy.js +42 -0
  24. package/src/account-rotation/strategies/sticky-strategy.js +97 -0
  25. package/src/claude-config.js +153 -0
  26. package/src/cli/accounts.js +557 -0
  27. package/src/direct-api.js +164 -0
  28. package/src/format-converter.js +420 -0
  29. package/src/index.js +46 -0
  30. package/src/kilo-api.js +68 -0
  31. package/src/kilo-format-converter.js +285 -0
  32. package/src/kilo-models.js +103 -0
  33. package/src/kilo-streamer.js +243 -0
  34. package/src/middleware/credentials.js +116 -0
  35. package/src/middleware/sse.js +96 -0
  36. package/src/model-api.js +189 -0
  37. package/src/model-mapper.js +157 -0
  38. package/src/oauth.js +666 -0
  39. package/src/response-streamer.js +409 -0
  40. package/src/routes/accounts-route.js +332 -0
  41. package/src/routes/api-routes.js +98 -0
  42. package/src/routes/chat-route.js +229 -0
  43. package/src/routes/claude-config-route.js +121 -0
  44. package/src/routes/logs-route.js +43 -0
  45. package/src/routes/messages-route.js +203 -0
  46. package/src/routes/models-route.js +119 -0
  47. package/src/routes/settings-route.js +143 -0
  48. package/src/security.js +142 -0
  49. package/src/server-settings.js +56 -0
  50. package/src/server.js +58 -0
  51. package/src/signature-cache.js +106 -0
  52. package/src/thinking-utils.js +312 -0
  53. package/src/utils/logger.js +156 -0
@@ -0,0 +1,164 @@
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
+ function parseResetTime(response, errorText) {
12
+ const retryAfter = response.headers?.get?.('retry-after');
13
+ if (retryAfter) {
14
+ const seconds = parseInt(retryAfter, 10);
15
+ if (!isNaN(seconds)) return seconds * 1000;
16
+ }
17
+
18
+ const ratelimitReset = response.headers?.get?.('x-ratelimit-reset');
19
+ if (ratelimitReset) {
20
+ const timestamp = parseInt(ratelimitReset, 10) * 1000;
21
+ const wait = timestamp - Date.now();
22
+ if (wait > 0) return wait;
23
+ }
24
+
25
+ if (errorText) {
26
+ const delayMatch = errorText.match(/quotaResetDelay[:\s"]+(\d+(?:\.\d+)?)(ms|s)/i);
27
+ if (delayMatch) {
28
+ const value = parseFloat(delayMatch[1]);
29
+ return delayMatch[2] === 's' ? value * 1000 : value;
30
+ }
31
+
32
+ const secMatch = errorText.match(/retry\s+(?:after\s+)?(\d+)\s*(?:sec|s\b)/i);
33
+ if (secMatch) {
34
+ return parseInt(secMatch[1], 10) * 1000;
35
+ }
36
+ }
37
+
38
+ return 60000;
39
+ }
40
+
41
+ /**
42
+ * Send a streaming request to ChatGPT API
43
+ */
44
+ export async function* sendMessageStream(anthropicRequest, accessToken, accountId, accountRotator = null, currentEmail = null) {
45
+ const modelId = anthropicRequest.model;
46
+ const request = convertAnthropicToResponsesAPI(anthropicRequest);
47
+
48
+ const response = await fetch(API_URL, {
49
+ method: 'POST',
50
+ headers: {
51
+ 'Authorization': `Bearer ${accessToken}`,
52
+ 'ChatGPT-Account-ID': accountId,
53
+ 'Content-Type': 'application/json',
54
+ 'Accept': 'text/event-stream'
55
+ },
56
+ body: JSON.stringify(request)
57
+ });
58
+
59
+ if (!response.ok) {
60
+ const errorText = await response.text();
61
+
62
+ if (response.status === 401) {
63
+ if (accountRotator && currentEmail) {
64
+ accountRotator.markInvalid(currentEmail, 'Token expired or revoked');
65
+ }
66
+ throw new Error('AUTH_EXPIRED: Token expired or revoked. Please re-authenticate.');
67
+ }
68
+
69
+ if (response.status === 429) {
70
+ const resetMs = parseResetTime(response, errorText);
71
+ if (accountRotator && currentEmail) {
72
+ accountRotator.markRateLimited(currentEmail, resetMs, modelId);
73
+ }
74
+ throw new Error(`RATE_LIMITED:${resetMs}:${errorText}`);
75
+ }
76
+
77
+ if (response.status === 403) {
78
+ if (errorText.includes('challenge') || errorText.includes('cloudflare')) {
79
+ throw new Error('CLOUDFLARE_BLOCKED: Request blocked by Cloudflare.');
80
+ }
81
+ throw new Error(`FORBIDDEN: ${errorText}`);
82
+ }
83
+
84
+ if (response.status === 400) {
85
+ throw new Error(`INVALID_REQUEST: ${errorText}`);
86
+ }
87
+
88
+ throw new Error(`API_ERROR: ${response.status} - ${errorText}`);
89
+ }
90
+
91
+ yield* streamResponsesAPI(response, anthropicRequest.model);
92
+ }
93
+
94
+ /**
95
+ * Send a non-streaming request to ChatGPT API
96
+ */
97
+ export async function sendMessage(anthropicRequest, accessToken, accountId) {
98
+ const request = convertAnthropicToResponsesAPI({
99
+ ...anthropicRequest,
100
+ stream: false
101
+ });
102
+
103
+ const response = await fetch(API_URL, {
104
+ method: 'POST',
105
+ headers: {
106
+ 'Authorization': `Bearer ${accessToken}`,
107
+ 'ChatGPT-Account-ID': accountId,
108
+ 'Content-Type': 'application/json',
109
+ 'Accept': 'text/event-stream'
110
+ },
111
+ body: JSON.stringify(request)
112
+ });
113
+
114
+ if (!response.ok) {
115
+ const errorText = await response.text();
116
+
117
+ if (response.status === 401) {
118
+ throw new Error('AUTH_EXPIRED: Token expired or revoked. Please re-authenticate.');
119
+ }
120
+
121
+ throw new Error(`API_ERROR: ${response.status} - ${errorText}`);
122
+ }
123
+
124
+ const apiResponse = await parseResponsesAPIResponse(response);
125
+
126
+ if (!apiResponse) {
127
+ return {
128
+ id: generateMessageId(),
129
+ type: 'message',
130
+ role: 'assistant',
131
+ content: [{ type: 'text', text: '' }],
132
+ model: anthropicRequest.model,
133
+ stop_reason: 'end_turn',
134
+ stop_sequence: null,
135
+ usage: { input_tokens: 0, output_tokens: 0 }
136
+ };
137
+ }
138
+
139
+ const content = convertOutputToAnthropic(apiResponse.output);
140
+ const stopReason = content.some(c => c.type === 'tool_use') ? 'tool_use' : 'end_turn';
141
+
142
+ return {
143
+ id: generateMessageId(),
144
+ type: 'message',
145
+ role: 'assistant',
146
+ content: content,
147
+ model: anthropicRequest.model,
148
+ stop_reason: stopReason,
149
+ stop_sequence: null,
150
+ usage: {
151
+ input_tokens: apiResponse.usage?.input_tokens || 0,
152
+ output_tokens: apiResponse.usage?.output_tokens || 0,
153
+ cache_read_input_tokens: apiResponse.usage?.cache_read_input_tokens || 0
154
+ }
155
+ };
156
+ }
157
+
158
+ export { parseResetTime };
159
+
160
+ export default {
161
+ sendMessageStream,
162
+ sendMessage,
163
+ parseResetTime
164
+ };
@@ -0,0 +1,420 @@
1
+ /**
2
+ * Format Converter
3
+ * Converts between Anthropic Messages API and OpenAI Responses API format
4
+ */
5
+
6
+ import crypto from 'crypto';
7
+ import { cleanCacheControl, processAssistantContent, hasUnsignedThinkingBlocks } from './thinking-utils.js';
8
+ import { getCachedSignature, cacheSignature, cacheThinkingSignature, SIGNATURE_CONSTANTS } from './signature-cache.js';
9
+ import { DEFAULT_OPENAI_MODEL } from './model-mapper.js';
10
+
11
+ const { MIN_SIGNATURE_LENGTH } = SIGNATURE_CONSTANTS;
12
+
13
+ /**
14
+ * Convert Anthropic tool ID to OpenAI fc_ format
15
+ * Deterministic: strips toolu_/call_ prefix, adds fc_ prefix
16
+ * @param {string} anthropicId - Original Anthropic tool ID (e.g., toolu_abc123)
17
+ * @returns {string} OpenAI fc_ format ID (e.g., fc_abc123)
18
+ */
19
+ function toOpenAIToolId(anthropicId) {
20
+ if (!anthropicId) return `fc_${crypto.randomBytes(12).toString('hex')}`;
21
+ if (anthropicId.startsWith('fc_')) return anthropicId;
22
+
23
+ // Strip known prefixes and add fc_ prefix
24
+ const baseId = anthropicId.replace(/^(call_|toolu_)/, '');
25
+ return `fc_${baseId}`;
26
+ }
27
+
28
+ /**
29
+ * Convert OpenAI fc_ ID back to Anthropic toolu_ format
30
+ * Deterministic: strips fc_ prefix, adds toolu_ prefix
31
+ * This is the inverse of toOpenAIToolId
32
+ * @param {string} openAIId - OpenAI fc_ format ID (e.g., fc_abc123)
33
+ * @returns {string} Anthropic toolu_ format ID (e.g., toolu_abc123)
34
+ */
35
+ function toAnthropicToolId(openAIId) {
36
+ if (!openAIId) return `toolu_${crypto.randomBytes(12).toString('hex')}`;
37
+ if (openAIId.startsWith('toolu_')) return openAIId;
38
+
39
+ // Strip fc_ prefix and add toolu_ prefix
40
+ const baseId = openAIId.replace(/^fc_/, '');
41
+ return `toolu_${baseId}`;
42
+ }
43
+
44
+ function extractSystemPrompt(system) {
45
+ if (!system) {
46
+ return undefined;
47
+ }
48
+
49
+ if (typeof system === 'string') {
50
+ return system;
51
+ }
52
+
53
+ if (Array.isArray(system)) {
54
+ const textParts = system
55
+ .filter(block => block.type === 'text')
56
+ .map(block => block.text);
57
+ return textParts.join('\n\n') || undefined;
58
+ }
59
+
60
+ return undefined;
61
+ }
62
+
63
+ /**
64
+ * Convert Anthropic Messages API request to OpenAI Responses API format
65
+ */
66
+ export function convertAnthropicToResponsesAPI(anthropicRequest) {
67
+ const { model, messages, system, tools, tool_choice } = anthropicRequest;
68
+
69
+ // [CRITICAL] Clean cache_control from all messages FIRST
70
+ // Claude Code CLI sends cache_control fields that the API rejects
71
+ const cleanedMessages = cleanCacheControl(messages || []);
72
+
73
+ const instructions = extractSystemPrompt(system);
74
+
75
+ const request = {
76
+ model: model || DEFAULT_OPENAI_MODEL,
77
+ input: convertMessagesToInput(cleanedMessages),
78
+ tools: tools ? convertAnthropicToolsToOpenAI(tools) : [],
79
+ tool_choice: tool_choice || 'auto',
80
+ parallel_tool_calls: true,
81
+ store: false,
82
+ stream: true,
83
+ include: []
84
+ };
85
+
86
+ if (instructions) {
87
+ request.instructions = instructions;
88
+ } else {
89
+ request.instructions = '';
90
+ }
91
+
92
+ return request;
93
+ }
94
+
95
+ /**
96
+ * Convert Anthropic messages to OpenAI Responses API input format
97
+ */
98
+ function convertMessagesToInput(messages) {
99
+ if (!Array.isArray(messages)) {
100
+ return [];
101
+ }
102
+
103
+ const input = [];
104
+
105
+ for (const msg of messages) {
106
+ if (msg.role === 'user') {
107
+ const { textParts, toolResults } = convertUserContent(msg.content);
108
+
109
+ if (textParts.length > 0) {
110
+ // API accepts: string OR array of {type: 'input_text', text: '...'}
111
+ const content = textParts.length === 1
112
+ ? textParts[0] // Use string for single text
113
+ : textParts.map(text => ({ type: 'input_text', text }));
114
+ input.push({
115
+ type: 'message',
116
+ role: 'user',
117
+ content
118
+ });
119
+ }
120
+
121
+ for (const result of toolResults) {
122
+ input.push(result);
123
+ }
124
+ } else if (msg.role === 'assistant') {
125
+ // Process assistant content: restore signatures, reorder, sanitize
126
+ let msgContent = msg.content;
127
+ if (Array.isArray(msgContent)) {
128
+ msgContent = processAssistantContent(msgContent);
129
+ }
130
+
131
+ const { textParts, toolCalls } = convertAssistantContentToOpenAI(msgContent);
132
+
133
+ if (textParts.length > 0) {
134
+ // API accepts: string OR array of {type: 'output_text', text: '...'}
135
+ const content = textParts.length === 1
136
+ ? textParts[0] // Use string for single text
137
+ : textParts.map(text => ({ type: 'output_text', text }));
138
+ input.push({
139
+ type: 'message',
140
+ role: 'assistant',
141
+ content
142
+ });
143
+ }
144
+
145
+ for (const call of toolCalls) {
146
+ input.push(call);
147
+ }
148
+ }
149
+ }
150
+
151
+ return input;
152
+ }
153
+
154
+ /**
155
+ * Convert user content, separating text and tool results
156
+ */
157
+ function convertUserContent(content) {
158
+ const textParts = [];
159
+ const toolResults = [];
160
+
161
+ if (typeof content === 'string') {
162
+ textParts.push(content);
163
+ } else if (Array.isArray(content)) {
164
+ for (const block of content) {
165
+ if (block.type === 'text') {
166
+ textParts.push(block.text);
167
+ } else if (block.type === 'tool_result') {
168
+ const outputContent = typeof block.content === 'string'
169
+ ? block.content
170
+ : Array.isArray(block.content)
171
+ ? block.content.filter(c => c.type === 'text').map(c => c.text).join('\n')
172
+ : JSON.stringify(block.content);
173
+
174
+ // Convert to OpenAI fc_ format
175
+ const callId = toOpenAIToolId(block.tool_use_id);
176
+
177
+ toolResults.push({
178
+ type: 'function_call_output',
179
+ call_id: callId,
180
+ output: block.is_error ? `Error: ${outputContent}` : outputContent
181
+ });
182
+ }
183
+ }
184
+ }
185
+
186
+ return { textParts, toolResults };
187
+ }
188
+
189
+ /**
190
+ * Convert Anthropic assistant content to OpenAI format
191
+ */
192
+ function convertAssistantContentToOpenAI(content) {
193
+ const textParts = [];
194
+ const toolCalls = [];
195
+
196
+ if (typeof content === 'string') {
197
+ textParts.push(content);
198
+ } else if (Array.isArray(content)) {
199
+ for (const block of content) {
200
+ if (block.type === 'text') {
201
+ textParts.push(block.text);
202
+ } else if (block.type === 'thinking') {
203
+ // Handle thinking blocks - they may have signatures we need to cache
204
+ if (block.signature && block.signature.length >= MIN_SIGNATURE_LENGTH) {
205
+ cacheThinkingSignature(block.signature, 'openai');
206
+ }
207
+ // For now, we don't include thinking in the output
208
+ // The API will regenerate thinking as needed
209
+ } else if (block.type === 'tool_use') {
210
+ // Convert to OpenAI fc_ format while preserving mapping
211
+ const openAIId = toOpenAIToolId(block.id);
212
+
213
+ // Restore thoughtSignature from cache if missing (Claude Code strips it)
214
+ let thoughtSignature = block.thoughtSignature;
215
+ if (!thoughtSignature && block.id) {
216
+ thoughtSignature = getCachedSignature(block.id);
217
+ if (thoughtSignature) {
218
+ console.log(`[FormatConverter] Restored signature from cache for tool: ${block.id}`);
219
+ }
220
+ }
221
+
222
+ // Cache the signature for future restoration (keyed by original ID)
223
+ if (thoughtSignature && thoughtSignature.length >= MIN_SIGNATURE_LENGTH) {
224
+ cacheSignature(block.id, thoughtSignature);
225
+ }
226
+
227
+ toolCalls.push({
228
+ type: 'function_call',
229
+ id: openAIId,
230
+ call_id: openAIId,
231
+ name: block.name,
232
+ arguments: typeof block.input === 'string'
233
+ ? block.input
234
+ : JSON.stringify(block.input)
235
+ });
236
+ }
237
+ }
238
+ }
239
+
240
+ return { textParts, toolCalls };
241
+ }
242
+
243
+ /**
244
+ * Convert Anthropic tools to OpenAI function format
245
+ */
246
+ function convertAnthropicToolsToOpenAI(tools) {
247
+ if (!Array.isArray(tools)) {
248
+ return [];
249
+ }
250
+
251
+ return tools.map(tool => ({
252
+ type: 'function',
253
+ name: tool.name,
254
+ description: tool.description || '',
255
+ parameters: sanitizeSchema(tool.input_schema || { type: 'object' })
256
+ }));
257
+ }
258
+
259
+ function sanitizeSchema(schema) {
260
+ if (typeof schema !== 'object' || schema === null) {
261
+ return { type: 'object' };
262
+ }
263
+
264
+ const result = {};
265
+
266
+ for (const [key, value] of Object.entries(schema)) {
267
+ if (key === 'const') {
268
+ result.enum = [value];
269
+ continue;
270
+ }
271
+
272
+ if ([
273
+ '$schema', '$id', '$ref', '$defs', '$comment',
274
+ 'additionalItems', 'definitions', 'examples',
275
+ 'minLength', 'maxLength', 'pattern', 'format',
276
+ 'minItems', 'maxItems', 'minimum', 'maximum',
277
+ 'exclusiveMinimum', 'exclusiveMaximum',
278
+ 'allOf', 'anyOf', 'oneOf', 'not'
279
+ ].includes(key)) {
280
+ continue;
281
+ }
282
+
283
+ if (key === 'additionalProperties' && typeof value === 'boolean') {
284
+ continue;
285
+ }
286
+
287
+ if (key === 'type' && Array.isArray(value)) {
288
+ const nonNullTypes = value.filter(t => t !== 'null');
289
+ result.type = nonNullTypes.length > 0 ? nonNullTypes[0] : 'string';
290
+ continue;
291
+ }
292
+
293
+ if (key === 'properties' && value && typeof value === 'object') {
294
+ result.properties = {};
295
+ for (const [propKey, propValue] of Object.entries(value)) {
296
+ result.properties[propKey] = sanitizeSchema(propValue);
297
+ }
298
+ continue;
299
+ }
300
+
301
+ if (key === 'items') {
302
+ if (Array.isArray(value)) {
303
+ result.items = value.map(item => sanitizeSchema(item));
304
+ } else if (typeof value === 'object') {
305
+ result.items = sanitizeSchema(value);
306
+ } else {
307
+ result.items = value;
308
+ }
309
+ continue;
310
+ }
311
+
312
+ if (key === 'required' && Array.isArray(value)) {
313
+ result.required = value;
314
+ continue;
315
+ }
316
+
317
+ if (key === 'enum' && Array.isArray(value)) {
318
+ result.enum = value;
319
+ continue;
320
+ }
321
+
322
+ if (['type', 'description', 'title'].includes(key)) {
323
+ result[key] = value;
324
+ }
325
+ }
326
+
327
+ if (!result.type) {
328
+ result.type = 'object';
329
+ }
330
+
331
+ if (result.type === 'object' && !result.properties) {
332
+ result.properties = {};
333
+ }
334
+
335
+ return result;
336
+ }
337
+
338
+ /**
339
+ * Convert OpenAI Responses API output to Anthropic content blocks
340
+ */
341
+ export function convertOutputToAnthropic(output) {
342
+ if (!Array.isArray(output)) {
343
+ return [{ type: 'text', text: '' }];
344
+ }
345
+
346
+ const content = [];
347
+
348
+ for (const item of output) {
349
+ if (item.type === 'message') {
350
+ for (const part of item.content || []) {
351
+ if (part.type === 'output_text') {
352
+ content.push({ type: 'text', text: part.text });
353
+ }
354
+ }
355
+ } else if (item.type === 'function_call') {
356
+ let input = {};
357
+ try {
358
+ input = typeof item.arguments === 'string'
359
+ ? JSON.parse(item.arguments)
360
+ : item.arguments || {};
361
+ } catch (e) {
362
+ input = {};
363
+ }
364
+
365
+ // Convert OpenAI fc_ ID back to original Anthropic ID
366
+ const openAIId = item.call_id || item.id;
367
+ const toolId = toAnthropicToolId(openAIId);
368
+
369
+ const toolUseBlock = {
370
+ type: 'tool_use',
371
+ id: toolId,
372
+ name: item.name,
373
+ input: input
374
+ };
375
+
376
+ // Cache signature if present (keyed by original Anthropic ID)
377
+ if (item.signature && item.signature.length >= MIN_SIGNATURE_LENGTH) {
378
+ toolUseBlock.thoughtSignature = item.signature;
379
+ cacheSignature(toolId, item.signature);
380
+ }
381
+
382
+ content.push(toolUseBlock);
383
+ } else if (item.type === 'reasoning') {
384
+ const signature = item.signature || '';
385
+
386
+ // Cache thinking signature
387
+ if (signature && signature.length >= MIN_SIGNATURE_LENGTH) {
388
+ cacheThinkingSignature(signature, 'openai');
389
+ }
390
+
391
+ content.push({
392
+ type: 'thinking',
393
+ thinking: item.text || item.content || '',
394
+ signature: signature
395
+ });
396
+ }
397
+ }
398
+
399
+ return content.length > 0 ? content : [{ type: 'text', text: '' }];
400
+ }
401
+
402
+ /**
403
+ * Generate Anthropic message ID
404
+ */
405
+ export function generateMessageId() {
406
+ return `msg_${crypto.randomBytes(16).toString('hex')}`;
407
+ }
408
+
409
+ export {
410
+ toOpenAIToolId,
411
+ toAnthropicToolId
412
+ };
413
+
414
+ export default {
415
+ convertAnthropicToResponsesAPI,
416
+ convertOutputToAnthropic,
417
+ generateMessageId,
418
+ toOpenAIToolId,
419
+ toAnthropicToolId
420
+ };
package/src/index.js ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Codex Claude Proxy
3
+ * Entry point
4
+ */
5
+
6
+ import { startServer } from './server.js';
7
+ import { logger } from './utils/logger.js';
8
+ import { getStatus, ACCOUNTS_FILE } from './account-manager.js';
9
+
10
+ const PORT = Number(process.env.PORT || 8081);
11
+ const HOST = process.env.HOST || '127.0.0.1';
12
+
13
+ startServer({ port: PORT, host: HOST });
14
+
15
+ console.log(`
16
+ ╔══════════════════════════════════════════════════════════════╗
17
+ ║ Codex Claude Proxy v1.0.6 ║
18
+ ║ (Direct API Mode) ║
19
+ ╠══════════════════════════════════════════════════════════════╣
20
+ ║ Server: http://${HOST}:${PORT} ║
21
+ ║ WebUI: http://${HOST}:${PORT} ║
22
+ ║ Health: http://${HOST}:${PORT}/health ║
23
+ ║ Accounts: http://${HOST}:${PORT}/accounts ║
24
+ ║ Logs: http://${HOST}:${PORT}/api/logs/stream ║
25
+ ╠══════════════════════════════════════════════════════════════╣
26
+ ║ Features: ║
27
+ ║ ✓ Native tool calling support ║
28
+ ║ ✓ Real-time streaming ║
29
+ ║ ✓ Multi-account management ║
30
+ ║ ✓ OpenAI & Anthropic API compatibility ║
31
+ ╠══════════════════════════════════════════════════════════════╣
32
+ ║ Support: ║
33
+ ║ ★ Give it a star on GitHub! ║
34
+ ║ https://github.com/surajmandalcell/codex-proxy ║
35
+ ╚══════════════════════════════════════════════════════════════╝
36
+ `);
37
+
38
+ const status = getStatus();
39
+ logger.info(`Accounts: ${status.total} total, Active: ${status.active || 'None'}`);
40
+
41
+ if (status.total === 0) {
42
+ logger.warn(`No accounts configured. Open http://${HOST}:${PORT} to add one.`);
43
+ }
44
+
45
+ // Expose config path in logs for convenience
46
+ logger.info(`Accounts config: ${ACCOUNTS_FILE}`);
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Kilo API client
3
+ */
4
+
5
+ import { convertAnthropicToOpenAIChat, convertOpenAIChatToAnthropic } from './kilo-format-converter.js';
6
+ import { streamOpenAIChat } from './kilo-streamer.js';
7
+
8
+ const KILO_API_URL = 'https://api.kilo.ai/api/openrouter/chat/completions';
9
+
10
+ const KILO_HEADERS = {
11
+ Authorization: 'Bearer anonymous',
12
+ 'User-Agent': 'opencode-kilo-provider',
13
+ 'HTTP-Referer': 'https://kilo.ai'
14
+ };
15
+
16
+ function buildError(status, message) {
17
+ const err = new Error(message);
18
+ err.status = status;
19
+ return err;
20
+ }
21
+
22
+ export async function* sendKiloMessageStream(anthropicRequest, targetModel) {
23
+ const requestBody = convertAnthropicToOpenAIChat(anthropicRequest, targetModel);
24
+
25
+ const response = await fetch(KILO_API_URL, {
26
+ method: 'POST',
27
+ headers: {
28
+ ...KILO_HEADERS,
29
+ 'Content-Type': 'application/json',
30
+ Accept: 'text/event-stream'
31
+ },
32
+ body: JSON.stringify(requestBody)
33
+ });
34
+
35
+ if (!response.ok) {
36
+ const errorText = await response.text();
37
+ throw buildError(response.status, `KILO_API_ERROR: ${response.status} - ${errorText}`);
38
+ }
39
+
40
+ yield* streamOpenAIChat(response, anthropicRequest.model);
41
+ }
42
+
43
+ export async function sendKiloMessage(anthropicRequest, targetModel) {
44
+ const requestBody = convertAnthropicToOpenAIChat({ ...anthropicRequest, stream: false }, targetModel);
45
+
46
+ const response = await fetch(KILO_API_URL, {
47
+ method: 'POST',
48
+ headers: {
49
+ ...KILO_HEADERS,
50
+ 'Content-Type': 'application/json',
51
+ Accept: 'application/json'
52
+ },
53
+ body: JSON.stringify({ ...requestBody, stream: false })
54
+ });
55
+
56
+ if (!response.ok) {
57
+ const errorText = await response.text();
58
+ throw buildError(response.status, `KILO_API_ERROR: ${response.status} - ${errorText}`);
59
+ }
60
+
61
+ const data = await response.json();
62
+ return convertOpenAIChatToAnthropic(data);
63
+ }
64
+
65
+ export default {
66
+ sendKiloMessageStream,
67
+ sendKiloMessage
68
+ };