claude-codex-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 +227 -0
- package/bin/cli.js +113 -0
- package/docs/ACCOUNTS.md +202 -0
- package/docs/API.md +244 -0
- package/docs/ARCHITECTURE.md +119 -0
- package/docs/CLAUDE_INTEGRATION.md +163 -0
- package/docs/OAUTH.md +83 -0
- package/docs/OPENCLAW.md +33 -0
- package/docs/legal.md +9 -0
- package/images/demo-screenshot.png +0 -0
- package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
- package/package.json +56 -0
- package/public/css/style.css +791 -0
- package/public/index.html +838 -0
- package/public/js/app.js +619 -0
- package/src/account-manager.js +526 -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 +154 -0
- package/src/cli/accounts.js +551 -0
- package/src/direct-api.js +214 -0
- package/src/format-converter.js +563 -0
- package/src/index.js +45 -0
- package/src/middleware/credentials.js +116 -0
- package/src/middleware/sse.js +130 -0
- package/src/model-api.js +189 -0
- package/src/model-mapper.js +88 -0
- package/src/oauth.js +623 -0
- package/src/response-streamer.js +469 -0
- package/src/routes/accounts-route.js +296 -0
- package/src/routes/api-routes.js +102 -0
- package/src/routes/chat-route.js +239 -0
- package/src/routes/claude-config-route.js +114 -0
- package/src/routes/logs-route.js +43 -0
- package/src/routes/messages-route.js +164 -0
- package/src/routes/models-route.js +115 -0
- package/src/routes/settings-route.js +154 -0
- package/src/server-settings.js +70 -0
- package/src/server.js +57 -0
- package/src/signature-cache.js +106 -0
- package/src/thinking-utils.js +312 -0
- package/src/utils/logger.js +170 -0
|
@@ -0,0 +1,563 @@
|
|
|
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
|
+
|
|
10
|
+
const { MIN_SIGNATURE_LENGTH } = SIGNATURE_CONSTANTS;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Convert Anthropic tool ID to OpenAI fc_ format
|
|
14
|
+
* Deterministic: strips toolu_/call_ prefix, adds fc_ prefix
|
|
15
|
+
* @param {string} anthropicId - Original Anthropic tool ID (e.g., toolu_abc123)
|
|
16
|
+
* @returns {string} OpenAI fc_ format ID (e.g., fc_abc123)
|
|
17
|
+
*/
|
|
18
|
+
function toOpenAIToolId(anthropicId) {
|
|
19
|
+
if (!anthropicId) return `fc_${crypto.randomBytes(12).toString('hex')}`;
|
|
20
|
+
if (anthropicId.startsWith('fc_')) return anthropicId;
|
|
21
|
+
|
|
22
|
+
// Strip known prefixes and add fc_ prefix
|
|
23
|
+
const baseId = anthropicId.replace(/^(call_|toolu_)/, '');
|
|
24
|
+
return `fc_${baseId}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Convert OpenAI fc_ ID back to Anthropic toolu_ format
|
|
29
|
+
* Deterministic: strips fc_ prefix, adds toolu_ prefix
|
|
30
|
+
* This is the inverse of toOpenAIToolId
|
|
31
|
+
* @param {string} openAIId - OpenAI fc_ format ID (e.g., fc_abc123)
|
|
32
|
+
* @returns {string} Anthropic toolu_ format ID (e.g., toolu_abc123)
|
|
33
|
+
*/
|
|
34
|
+
function toAnthropicToolId(openAIId) {
|
|
35
|
+
if (!openAIId) return `toolu_${crypto.randomBytes(12).toString('hex')}`;
|
|
36
|
+
if (openAIId.startsWith('toolu_')) return openAIId;
|
|
37
|
+
|
|
38
|
+
// Strip fc_ prefix and add toolu_ prefix
|
|
39
|
+
const baseId = openAIId.replace(/^fc_/, '');
|
|
40
|
+
return `toolu_${baseId}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function extractSystemPrompt(system) {
|
|
44
|
+
if (!system) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof system === 'string') {
|
|
49
|
+
return system;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (Array.isArray(system)) {
|
|
53
|
+
const textParts = system
|
|
54
|
+
.filter(block => block.type === 'text')
|
|
55
|
+
.map(block => block.text);
|
|
56
|
+
return textParts.join('\n\n') || undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function convertAnthropicImageToResponsesInputImage(block) {
|
|
63
|
+
const source = block?.source;
|
|
64
|
+
if (!source) return null;
|
|
65
|
+
|
|
66
|
+
if (source.type === 'base64' && source.data) {
|
|
67
|
+
const mediaType = source.media_type || 'image/png';
|
|
68
|
+
return {
|
|
69
|
+
type: 'input_image',
|
|
70
|
+
image_url: `data:${mediaType};base64,${source.data}`
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (source.type === 'url' && source.url) {
|
|
75
|
+
return {
|
|
76
|
+
type: 'input_image',
|
|
77
|
+
image_url: source.url
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function convertToolResultContentToResponsesOutput(block) {
|
|
85
|
+
if (typeof block.content === 'string') {
|
|
86
|
+
return block.content;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (Array.isArray(block.content)) {
|
|
90
|
+
const output = [];
|
|
91
|
+
|
|
92
|
+
for (const item of block.content) {
|
|
93
|
+
if (item.type === 'text') {
|
|
94
|
+
output.push({
|
|
95
|
+
type: 'input_text',
|
|
96
|
+
text: item.text || ''
|
|
97
|
+
});
|
|
98
|
+
} else if (item.type === 'image') {
|
|
99
|
+
const image = convertAnthropicImageToResponsesInputImage(item);
|
|
100
|
+
if (image) output.push(image);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (output.length === 0) return '';
|
|
105
|
+
|
|
106
|
+
// Preserve legacy behavior for text-only tool results.
|
|
107
|
+
if (output.length === 1 && output[0].type === 'input_text') {
|
|
108
|
+
return output[0].text;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return output;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (block.content && typeof block.content === 'object') {
|
|
115
|
+
return JSON.stringify(block.content);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function prefixToolErrorOutput(output) {
|
|
122
|
+
if (typeof output === 'string') {
|
|
123
|
+
return `Error: ${output}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (Array.isArray(output)) {
|
|
127
|
+
return [
|
|
128
|
+
{ type: 'input_text', text: 'Error:' },
|
|
129
|
+
...output
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return output;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Convert Anthropic Messages API request to OpenAI Responses API format
|
|
138
|
+
*/
|
|
139
|
+
export function convertAnthropicToResponsesAPI(anthropicRequest) {
|
|
140
|
+
const { model, messages, system, tools, tool_choice } = anthropicRequest;
|
|
141
|
+
|
|
142
|
+
// [CRITICAL] Clean cache_control from all messages FIRST
|
|
143
|
+
// Claude Code CLI sends cache_control fields that the API rejects
|
|
144
|
+
const cleanedMessages = cleanCacheControl(messages || []);
|
|
145
|
+
|
|
146
|
+
const instructions = extractSystemPrompt(system);
|
|
147
|
+
|
|
148
|
+
const request = {
|
|
149
|
+
model: model || 'gpt-5.2-codex',
|
|
150
|
+
input: convertMessagesToInput(cleanedMessages),
|
|
151
|
+
tools: tools ? convertAnthropicToolsToOpenAI(tools) : [],
|
|
152
|
+
tool_choice: tool_choice || 'auto',
|
|
153
|
+
parallel_tool_calls: true,
|
|
154
|
+
store: false,
|
|
155
|
+
stream: true,
|
|
156
|
+
include: []
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
if (instructions) {
|
|
160
|
+
request.instructions = instructions;
|
|
161
|
+
} else {
|
|
162
|
+
request.instructions = '';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return request;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Convert Anthropic messages to OpenAI Responses API input format
|
|
170
|
+
*/
|
|
171
|
+
function convertMessagesToInput(messages) {
|
|
172
|
+
if (!Array.isArray(messages)) {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const input = [];
|
|
177
|
+
|
|
178
|
+
for (const msg of messages) {
|
|
179
|
+
if (msg.role === 'user') {
|
|
180
|
+
const { textParts, toolResults } = convertUserContent(msg.content);
|
|
181
|
+
|
|
182
|
+
if (textParts.length > 0) {
|
|
183
|
+
// API accepts: string OR array of {type: 'input_text', text: '...'}
|
|
184
|
+
const content = textParts.length === 1
|
|
185
|
+
? textParts[0] // Use string for single text
|
|
186
|
+
: textParts.map(text => ({ type: 'input_text', text }));
|
|
187
|
+
input.push({
|
|
188
|
+
type: 'message',
|
|
189
|
+
role: 'user',
|
|
190
|
+
content
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const result of toolResults) {
|
|
195
|
+
input.push(result);
|
|
196
|
+
}
|
|
197
|
+
} else if (msg.role === 'assistant') {
|
|
198
|
+
// Process assistant content: restore signatures, reorder, sanitize
|
|
199
|
+
let msgContent = msg.content;
|
|
200
|
+
if (Array.isArray(msgContent)) {
|
|
201
|
+
msgContent = processAssistantContent(msgContent);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const { textParts, toolCalls } = convertAssistantContentToOpenAI(msgContent);
|
|
205
|
+
|
|
206
|
+
if (textParts.length > 0) {
|
|
207
|
+
// API accepts: string OR array of {type: 'output_text', text: '...'}
|
|
208
|
+
const content = textParts.length === 1
|
|
209
|
+
? textParts[0] // Use string for single text
|
|
210
|
+
: textParts.map(text => ({ type: 'output_text', text }));
|
|
211
|
+
input.push({
|
|
212
|
+
type: 'message',
|
|
213
|
+
role: 'assistant',
|
|
214
|
+
content
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const call of toolCalls) {
|
|
219
|
+
input.push(call);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return input;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Convert user content, separating text and tool results
|
|
229
|
+
*/
|
|
230
|
+
function convertUserContent(content) {
|
|
231
|
+
const textParts = [];
|
|
232
|
+
const toolResults = [];
|
|
233
|
+
|
|
234
|
+
if (typeof content === 'string') {
|
|
235
|
+
textParts.push(content);
|
|
236
|
+
} else if (Array.isArray(content)) {
|
|
237
|
+
for (const block of content) {
|
|
238
|
+
if (block.type === 'text') {
|
|
239
|
+
textParts.push(block.text);
|
|
240
|
+
} else if (block.type === 'tool_result') {
|
|
241
|
+
const outputContent = convertToolResultContentToResponsesOutput(block);
|
|
242
|
+
|
|
243
|
+
// Convert to OpenAI fc_ format
|
|
244
|
+
const callId = toOpenAIToolId(block.tool_use_id);
|
|
245
|
+
|
|
246
|
+
toolResults.push({
|
|
247
|
+
type: 'function_call_output',
|
|
248
|
+
call_id: callId,
|
|
249
|
+
output: block.is_error ? prefixToolErrorOutput(outputContent) : outputContent
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { textParts, toolResults };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Convert Anthropic assistant content to OpenAI format
|
|
260
|
+
*/
|
|
261
|
+
function convertAssistantContentToOpenAI(content) {
|
|
262
|
+
const textParts = [];
|
|
263
|
+
const toolCalls = [];
|
|
264
|
+
|
|
265
|
+
if (typeof content === 'string') {
|
|
266
|
+
textParts.push(content);
|
|
267
|
+
} else if (Array.isArray(content)) {
|
|
268
|
+
for (const block of content) {
|
|
269
|
+
if (block.type === 'text') {
|
|
270
|
+
textParts.push(block.text);
|
|
271
|
+
} else if (block.type === 'thinking') {
|
|
272
|
+
// Handle thinking blocks - they may have signatures we need to cache
|
|
273
|
+
if (block.signature && block.signature.length >= MIN_SIGNATURE_LENGTH) {
|
|
274
|
+
cacheThinkingSignature(block.signature, 'openai');
|
|
275
|
+
}
|
|
276
|
+
// For now, we don't include thinking in the output
|
|
277
|
+
// The API will regenerate thinking as needed
|
|
278
|
+
} else if (block.type === 'tool_use') {
|
|
279
|
+
// Convert to OpenAI fc_ format while preserving mapping
|
|
280
|
+
const openAIId = toOpenAIToolId(block.id);
|
|
281
|
+
|
|
282
|
+
// Restore thoughtSignature from cache if missing (Claude Code strips it)
|
|
283
|
+
let thoughtSignature = block.thoughtSignature;
|
|
284
|
+
if (!thoughtSignature && block.id) {
|
|
285
|
+
thoughtSignature = getCachedSignature(block.id);
|
|
286
|
+
if (thoughtSignature) {
|
|
287
|
+
console.log(`[FormatConverter] Restored signature from cache for tool: ${block.id}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Cache the signature for future restoration (keyed by original ID)
|
|
292
|
+
if (thoughtSignature && thoughtSignature.length >= MIN_SIGNATURE_LENGTH) {
|
|
293
|
+
cacheSignature(block.id, thoughtSignature);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
toolCalls.push({
|
|
297
|
+
type: 'function_call',
|
|
298
|
+
id: openAIId,
|
|
299
|
+
call_id: openAIId,
|
|
300
|
+
name: block.name,
|
|
301
|
+
arguments: typeof block.input === 'string'
|
|
302
|
+
? block.input
|
|
303
|
+
: JSON.stringify(block.input)
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return { textParts, toolCalls };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Convert Anthropic tools to OpenAI function format
|
|
314
|
+
*/
|
|
315
|
+
function convertAnthropicToolsToOpenAI(tools) {
|
|
316
|
+
if (!Array.isArray(tools)) {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return tools.map(tool => ({
|
|
321
|
+
type: 'function',
|
|
322
|
+
name: tool.name,
|
|
323
|
+
description: tool.description || '',
|
|
324
|
+
parameters: sanitizeSchema(tool.input_schema || { type: 'object' })
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function sanitizeAnthropicToolInput(name, input) {
|
|
329
|
+
if (name !== 'Read') return input;
|
|
330
|
+
|
|
331
|
+
if (
|
|
332
|
+
input &&
|
|
333
|
+
typeof input === 'object' &&
|
|
334
|
+
!Array.isArray(input) &&
|
|
335
|
+
input.pages === ''
|
|
336
|
+
) {
|
|
337
|
+
const { pages, ...rest } = input;
|
|
338
|
+
return rest;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return input;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function sanitizeAnthropicToolInputJson(name, raw) {
|
|
345
|
+
if (name !== 'Read' || !raw) return raw;
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const input = JSON.parse(raw);
|
|
349
|
+
return JSON.stringify(sanitizeAnthropicToolInput(name, input));
|
|
350
|
+
} catch {
|
|
351
|
+
return raw;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function flattenStringEnumUnion(variants) {
|
|
356
|
+
if (!Array.isArray(variants)) return null;
|
|
357
|
+
|
|
358
|
+
const values = [];
|
|
359
|
+
|
|
360
|
+
for (const variant of variants) {
|
|
361
|
+
if (!variant || typeof variant !== 'object') return null;
|
|
362
|
+
|
|
363
|
+
if (typeof variant.const === 'string') {
|
|
364
|
+
values.push(variant.const);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (
|
|
369
|
+
Array.isArray(variant.enum) &&
|
|
370
|
+
variant.enum.every(value => typeof value === 'string')
|
|
371
|
+
) {
|
|
372
|
+
values.push(...variant.enum);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return [...new Set(values)];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function sanitizeSchema(schema) {
|
|
383
|
+
if (typeof schema !== 'object' || schema === null) {
|
|
384
|
+
return { type: 'object' };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const unionVariants = schema.anyOf || schema.oneOf;
|
|
388
|
+
const flattenedEnum = flattenStringEnumUnion(unionVariants);
|
|
389
|
+
if (flattenedEnum) {
|
|
390
|
+
return {
|
|
391
|
+
type: 'string',
|
|
392
|
+
enum: flattenedEnum,
|
|
393
|
+
...(schema.description ? { description: schema.description } : {}),
|
|
394
|
+
...(schema.title ? { title: schema.title } : {})
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const result = {};
|
|
399
|
+
|
|
400
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
401
|
+
if (key === 'const') {
|
|
402
|
+
result.enum = [value];
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (['allOf', 'anyOf', 'oneOf'].includes(key) && Array.isArray(value)) {
|
|
407
|
+
result[key] = value.map(item => sanitizeSchema(item));
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if ([
|
|
412
|
+
'$schema', '$id', '$ref', '$defs', '$comment',
|
|
413
|
+
'additionalItems', 'definitions', 'examples',
|
|
414
|
+
'pattern', 'format',
|
|
415
|
+
'minItems', 'maxItems', 'minimum', 'maximum',
|
|
416
|
+
'exclusiveMinimum', 'exclusiveMaximum',
|
|
417
|
+
'not'
|
|
418
|
+
].includes(key)) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (key === 'additionalProperties' && typeof value === 'boolean') {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (key === 'type' && Array.isArray(value)) {
|
|
427
|
+
const nonNullTypes = value.filter(t => t !== 'null');
|
|
428
|
+
result.type = nonNullTypes.length > 0 ? nonNullTypes[0] : 'string';
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (key === 'properties' && value && typeof value === 'object') {
|
|
433
|
+
result.properties = {};
|
|
434
|
+
for (const [propKey, propValue] of Object.entries(value)) {
|
|
435
|
+
result.properties[propKey] = sanitizeSchema(propValue);
|
|
436
|
+
}
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (key === 'items') {
|
|
441
|
+
if (Array.isArray(value)) {
|
|
442
|
+
result.items = value.map(item => sanitizeSchema(item));
|
|
443
|
+
} else if (typeof value === 'object') {
|
|
444
|
+
result.items = sanitizeSchema(value);
|
|
445
|
+
} else {
|
|
446
|
+
result.items = value;
|
|
447
|
+
}
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (key === 'required' && Array.isArray(value)) {
|
|
452
|
+
result.required = value;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (key === 'enum' && Array.isArray(value)) {
|
|
457
|
+
result.enum = value;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (['type', 'description', 'title'].includes(key)) {
|
|
462
|
+
result[key] = value;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const hasSchemaShape = result.enum || result.allOf || result.anyOf || result.oneOf;
|
|
467
|
+
|
|
468
|
+
if (!result.type && !hasSchemaShape) {
|
|
469
|
+
result.type = 'object';
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (result.type === 'object' && !result.properties) {
|
|
473
|
+
result.properties = {};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return result;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Convert OpenAI Responses API output to Anthropic content blocks
|
|
481
|
+
*/
|
|
482
|
+
export function convertOutputToAnthropic(output) {
|
|
483
|
+
if (!Array.isArray(output)) {
|
|
484
|
+
return [{ type: 'text', text: '' }];
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const content = [];
|
|
488
|
+
|
|
489
|
+
for (const item of output) {
|
|
490
|
+
if (item.type === 'message') {
|
|
491
|
+
for (const part of item.content || []) {
|
|
492
|
+
if (part.type === 'output_text') {
|
|
493
|
+
content.push({ type: 'text', text: part.text });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
} else if (item.type === 'function_call') {
|
|
497
|
+
let input = {};
|
|
498
|
+
try {
|
|
499
|
+
input = typeof item.arguments === 'string'
|
|
500
|
+
? JSON.parse(item.arguments)
|
|
501
|
+
: item.arguments || {};
|
|
502
|
+
} catch (e) {
|
|
503
|
+
input = {};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Convert OpenAI fc_ ID back to original Anthropic ID
|
|
507
|
+
const openAIId = item.call_id || item.id;
|
|
508
|
+
const toolId = toAnthropicToolId(openAIId);
|
|
509
|
+
|
|
510
|
+
const toolUseBlock = {
|
|
511
|
+
type: 'tool_use',
|
|
512
|
+
id: toolId,
|
|
513
|
+
name: item.name,
|
|
514
|
+
input: sanitizeAnthropicToolInput(item.name, input)
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// Cache signature if present (keyed by original Anthropic ID)
|
|
518
|
+
if (item.signature && item.signature.length >= MIN_SIGNATURE_LENGTH) {
|
|
519
|
+
toolUseBlock.thoughtSignature = item.signature;
|
|
520
|
+
cacheSignature(toolId, item.signature);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
content.push(toolUseBlock);
|
|
524
|
+
} else if (item.type === 'reasoning') {
|
|
525
|
+
const signature = item.signature || '';
|
|
526
|
+
|
|
527
|
+
// Cache thinking signature
|
|
528
|
+
if (signature && signature.length >= MIN_SIGNATURE_LENGTH) {
|
|
529
|
+
cacheThinkingSignature(signature, 'openai');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
content.push({
|
|
533
|
+
type: 'thinking',
|
|
534
|
+
thinking: item.text || item.content || '',
|
|
535
|
+
signature: signature
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return content.length > 0 ? content : [{ type: 'text', text: '' }];
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Generate Anthropic message ID
|
|
545
|
+
*/
|
|
546
|
+
export function generateMessageId() {
|
|
547
|
+
return `msg_${crypto.randomBytes(16).toString('hex')}`;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export {
|
|
551
|
+
sanitizeAnthropicToolInput,
|
|
552
|
+
sanitizeAnthropicToolInputJson,
|
|
553
|
+
toOpenAIToolId,
|
|
554
|
+
toAnthropicToolId
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
export default {
|
|
558
|
+
convertAnthropicToResponsesAPI,
|
|
559
|
+
convertOutputToAnthropic,
|
|
560
|
+
generateMessageId,
|
|
561
|
+
toOpenAIToolId,
|
|
562
|
+
toAnthropicToolId
|
|
563
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
|
|
12
|
+
startServer({ port: PORT });
|
|
13
|
+
|
|
14
|
+
console.log(`
|
|
15
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
16
|
+
║ Codex Claude Proxy v1.0.5 ║
|
|
17
|
+
║ (Direct API Mode) ║
|
|
18
|
+
╠══════════════════════════════════════════════════════════════╣
|
|
19
|
+
║ Server: http://localhost:${PORT} ║
|
|
20
|
+
║ WebUI: http://localhost:${PORT} ║
|
|
21
|
+
║ Health: http://localhost:${PORT}/health ║
|
|
22
|
+
║ Accounts: http://localhost:${PORT}/accounts ║
|
|
23
|
+
║ Logs: http://localhost:${PORT}/api/logs/stream ║
|
|
24
|
+
╠══════════════════════════════════════════════════════════════╣
|
|
25
|
+
║ Features: ║
|
|
26
|
+
║ ✓ Native tool calling support ║
|
|
27
|
+
║ ✓ Real-time streaming ║
|
|
28
|
+
║ ✓ Multi-account management ║
|
|
29
|
+
║ ✓ OpenAI & Anthropic API compatibility ║
|
|
30
|
+
╠══════════════════════════════════════════════════════════════╣
|
|
31
|
+
║ Support: ║
|
|
32
|
+
║ ★ Give it a star on GitHub! ║
|
|
33
|
+
║ https://github.com/Ayush-Kotlin-Dev/codex-claude-proxy ║
|
|
34
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
35
|
+
`);
|
|
36
|
+
|
|
37
|
+
const status = getStatus();
|
|
38
|
+
logger.info(`Accounts: ${status.total} total, Active: ${status.active || 'None'}`);
|
|
39
|
+
|
|
40
|
+
if (status.total === 0) {
|
|
41
|
+
logger.warn(`No accounts configured. Open http://localhost:${PORT} to add one.`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Expose config path in logs for convenience
|
|
45
|
+
logger.info(`Accounts config: ${ACCOUNTS_FILE}`);
|