@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.
- package/LICENSE +21 -0
- package/README.md +199 -0
- package/bin/cli.js +118 -0
- package/docs/ACCOUNTS.md +202 -0
- package/docs/API.md +289 -0
- package/docs/ARCHITECTURE.md +129 -0
- package/docs/CLAUDE_INTEGRATION.md +163 -0
- package/docs/OAUTH.md +85 -0
- package/docs/OPENCLAW.md +34 -0
- package/docs/legal.md +11 -0
- package/images/dashboard-screenshot.png +0 -0
- package/images/demo-screenshot.png +0 -0
- package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
- package/package.json +61 -0
- package/public/css/style.css +1502 -0
- package/public/index.html +827 -0
- package/public/js/app.js +601 -0
- package/src/account-manager.js +528 -0
- package/src/account-rotation/index.js +93 -0
- package/src/account-rotation/rate-limits.js +293 -0
- package/src/account-rotation/strategies/base-strategy.js +48 -0
- package/src/account-rotation/strategies/index.js +31 -0
- package/src/account-rotation/strategies/round-robin-strategy.js +42 -0
- package/src/account-rotation/strategies/sticky-strategy.js +97 -0
- package/src/claude-config.js +153 -0
- package/src/cli/accounts.js +557 -0
- package/src/direct-api.js +164 -0
- package/src/format-converter.js +420 -0
- package/src/index.js +46 -0
- package/src/kilo-api.js +68 -0
- package/src/kilo-format-converter.js +285 -0
- package/src/kilo-models.js +103 -0
- package/src/kilo-streamer.js +243 -0
- package/src/middleware/credentials.js +116 -0
- package/src/middleware/sse.js +96 -0
- package/src/model-api.js +189 -0
- package/src/model-mapper.js +157 -0
- package/src/oauth.js +666 -0
- package/src/response-streamer.js +409 -0
- package/src/routes/accounts-route.js +332 -0
- package/src/routes/api-routes.js +98 -0
- package/src/routes/chat-route.js +229 -0
- package/src/routes/claude-config-route.js +121 -0
- package/src/routes/logs-route.js +43 -0
- package/src/routes/messages-route.js +203 -0
- package/src/routes/models-route.js +119 -0
- package/src/routes/settings-route.js +143 -0
- package/src/security.js +142 -0
- package/src/server-settings.js +56 -0
- package/src/server.js +58 -0
- package/src/signature-cache.js +106 -0
- package/src/thinking-utils.js +312 -0
- 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}`);
|
package/src/kilo-api.js
ADDED
|
@@ -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
|
+
};
|