@h1d3rone/claude-proxy 0.1.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.
@@ -0,0 +1,533 @@
1
+ const crypto = require("crypto");
2
+
3
+ const CONSTANTS = {
4
+ ROLE_USER: "user",
5
+ ROLE_ASSISTANT: "assistant",
6
+ ROLE_SYSTEM: "system",
7
+ ROLE_TOOL: "tool",
8
+ CONTENT_TEXT: "text",
9
+ CONTENT_IMAGE: "image",
10
+ CONTENT_TOOL_USE: "tool_use",
11
+ CONTENT_TOOL_RESULT: "tool_result",
12
+ TOOL_FUNCTION: "function",
13
+ STOP_END_TURN: "end_turn",
14
+ STOP_MAX_TOKENS: "max_tokens",
15
+ STOP_TOOL_USE: "tool_use",
16
+ EVENT_MESSAGE_START: "message_start",
17
+ EVENT_MESSAGE_STOP: "message_stop",
18
+ EVENT_MESSAGE_DELTA: "message_delta",
19
+ EVENT_CONTENT_BLOCK_START: "content_block_start",
20
+ EVENT_CONTENT_BLOCK_STOP: "content_block_stop",
21
+ EVENT_CONTENT_BLOCK_DELTA: "content_block_delta",
22
+ EVENT_PING: "ping",
23
+ DELTA_TEXT: "text_delta",
24
+ DELTA_INPUT_JSON: "input_json_delta"
25
+ };
26
+
27
+ function randomMessageId(prefix = "msg") {
28
+ return `${prefix}_${crypto.randomBytes(12).toString("hex")}`;
29
+ }
30
+
31
+ function mapClaudeModelToOpenAI(claudeModel, config) {
32
+ if (!claudeModel) {
33
+ return config.big_model;
34
+ }
35
+
36
+ const model = String(claudeModel);
37
+ if (
38
+ model.startsWith("gpt-") ||
39
+ model.startsWith("o1-") ||
40
+ model.startsWith("o3-") ||
41
+ model.startsWith("ep-") ||
42
+ model.startsWith("doubao-") ||
43
+ model.startsWith("deepseek-")
44
+ ) {
45
+ return model;
46
+ }
47
+
48
+ const lower = model.toLowerCase();
49
+ if (lower.includes("haiku")) {
50
+ return config.small_model;
51
+ }
52
+ if (lower.includes("sonnet")) {
53
+ return config.middle_model;
54
+ }
55
+ if (lower.includes("opus")) {
56
+ return config.big_model;
57
+ }
58
+ return config.big_model;
59
+ }
60
+
61
+ function convertSystemPrompt(system) {
62
+ if (!system) {
63
+ return null;
64
+ }
65
+
66
+ if (typeof system === "string") {
67
+ return system.trim() || null;
68
+ }
69
+
70
+ if (Array.isArray(system)) {
71
+ const blocks = system
72
+ .filter((block) => block && block.type === CONSTANTS.CONTENT_TEXT)
73
+ .map((block) => block.text || "")
74
+ .filter(Boolean);
75
+ return blocks.join("\n\n").trim() || null;
76
+ }
77
+
78
+ return null;
79
+ }
80
+
81
+ function convertClaudeUserMessage(message) {
82
+ if (typeof message.content === "string") {
83
+ return { role: CONSTANTS.ROLE_USER, content: message.content };
84
+ }
85
+
86
+ const content = [];
87
+ for (const block of message.content || []) {
88
+ if (!block || !block.type) {
89
+ continue;
90
+ }
91
+ if (block.type === CONSTANTS.CONTENT_TEXT) {
92
+ content.push({ type: "text", text: block.text || "" });
93
+ }
94
+ if (
95
+ block.type === CONSTANTS.CONTENT_IMAGE &&
96
+ block.source &&
97
+ block.source.type === "base64" &&
98
+ block.source.media_type &&
99
+ block.source.data
100
+ ) {
101
+ content.push({
102
+ type: "image_url",
103
+ image_url: {
104
+ url: `data:${block.source.media_type};base64,${block.source.data}`
105
+ }
106
+ });
107
+ }
108
+ }
109
+
110
+ if (content.length === 1 && content[0].type === "text") {
111
+ return { role: CONSTANTS.ROLE_USER, content: content[0].text };
112
+ }
113
+
114
+ return { role: CONSTANTS.ROLE_USER, content };
115
+ }
116
+
117
+ function convertClaudeAssistantMessage(message) {
118
+ if (typeof message.content === "string") {
119
+ return { role: CONSTANTS.ROLE_ASSISTANT, content: message.content };
120
+ }
121
+
122
+ const textParts = [];
123
+ const toolCalls = [];
124
+
125
+ for (const block of message.content || []) {
126
+ if (!block || !block.type) {
127
+ continue;
128
+ }
129
+ if (block.type === CONSTANTS.CONTENT_TEXT) {
130
+ textParts.push(block.text || "");
131
+ }
132
+ if (block.type === CONSTANTS.CONTENT_TOOL_USE) {
133
+ toolCalls.push({
134
+ id: block.id || randomMessageId("tool"),
135
+ type: CONSTANTS.TOOL_FUNCTION,
136
+ function: {
137
+ name: block.name || "",
138
+ arguments: JSON.stringify(block.input || {})
139
+ }
140
+ });
141
+ }
142
+ }
143
+
144
+ const payload = {
145
+ role: CONSTANTS.ROLE_ASSISTANT,
146
+ content: textParts.length > 0 ? textParts.join("") : null
147
+ };
148
+
149
+ if (toolCalls.length > 0) {
150
+ payload.tool_calls = toolCalls;
151
+ }
152
+
153
+ return payload;
154
+ }
155
+
156
+ function normalizeToolResultContent(content) {
157
+ if (content == null) {
158
+ return "No content provided";
159
+ }
160
+ if (typeof content === "string") {
161
+ return content;
162
+ }
163
+ if (Array.isArray(content)) {
164
+ return content
165
+ .map((item) => {
166
+ if (typeof item === "string") {
167
+ return item;
168
+ }
169
+ if (item && typeof item === "object" && item.type === CONSTANTS.CONTENT_TEXT) {
170
+ return item.text || "";
171
+ }
172
+ return JSON.stringify(item);
173
+ })
174
+ .join("\n")
175
+ .trim();
176
+ }
177
+ return JSON.stringify(content);
178
+ }
179
+
180
+ function convertClaudeToolResults(message) {
181
+ const toolMessages = [];
182
+ for (const block of message.content || []) {
183
+ if (!block || block.type !== CONSTANTS.CONTENT_TOOL_RESULT) {
184
+ continue;
185
+ }
186
+ toolMessages.push({
187
+ role: CONSTANTS.ROLE_TOOL,
188
+ tool_call_id: block.tool_use_id,
189
+ content: normalizeToolResultContent(block.content)
190
+ });
191
+ }
192
+ return toolMessages;
193
+ }
194
+
195
+ function convertClaudeToOpenAI(claudeRequest, config) {
196
+ const messages = [];
197
+ const systemPrompt = convertSystemPrompt(claudeRequest.system);
198
+ if (systemPrompt) {
199
+ messages.push({ role: CONSTANTS.ROLE_SYSTEM, content: systemPrompt });
200
+ }
201
+
202
+ for (let index = 0; index < (claudeRequest.messages || []).length; index += 1) {
203
+ const message = claudeRequest.messages[index];
204
+ if (!message) {
205
+ continue;
206
+ }
207
+
208
+ if (message.role === CONSTANTS.ROLE_USER) {
209
+ messages.push(convertClaudeUserMessage(message));
210
+ continue;
211
+ }
212
+
213
+ if (message.role === CONSTANTS.ROLE_ASSISTANT) {
214
+ messages.push(convertClaudeAssistantMessage(message));
215
+
216
+ const nextMessage = claudeRequest.messages[index + 1];
217
+ const hasToolResult =
218
+ nextMessage &&
219
+ nextMessage.role === CONSTANTS.ROLE_USER &&
220
+ Array.isArray(nextMessage.content) &&
221
+ nextMessage.content.some((block) => block && block.type === CONSTANTS.CONTENT_TOOL_RESULT);
222
+
223
+ if (hasToolResult) {
224
+ index += 1;
225
+ messages.push(...convertClaudeToolResults(nextMessage));
226
+ }
227
+ }
228
+ }
229
+
230
+ const payload = {
231
+ model: mapClaudeModelToOpenAI(claudeRequest.model, config),
232
+ messages,
233
+ max_tokens: Math.max(1, Math.min(Number(claudeRequest.max_tokens || 4096), 128000)),
234
+ temperature: claudeRequest.temperature,
235
+ stream: Boolean(claudeRequest.stream)
236
+ };
237
+
238
+ if (Array.isArray(claudeRequest.stop_sequences) && claudeRequest.stop_sequences.length > 0) {
239
+ payload.stop = claudeRequest.stop_sequences;
240
+ }
241
+
242
+ if (typeof claudeRequest.top_p === "number") {
243
+ payload.top_p = claudeRequest.top_p;
244
+ }
245
+
246
+ if (Array.isArray(claudeRequest.tools) && claudeRequest.tools.length > 0) {
247
+ payload.tools = claudeRequest.tools
248
+ .filter((tool) => tool && tool.name)
249
+ .map((tool) => ({
250
+ type: CONSTANTS.TOOL_FUNCTION,
251
+ function: {
252
+ name: tool.name,
253
+ description: tool.description || "",
254
+ parameters: tool.input_schema || { type: "object", properties: {} }
255
+ }
256
+ }));
257
+ }
258
+
259
+ if (claudeRequest.tool_choice && typeof claudeRequest.tool_choice === "object") {
260
+ const type = claudeRequest.tool_choice.type;
261
+ if (type === "tool" && claudeRequest.tool_choice.name) {
262
+ payload.tool_choice = {
263
+ type: CONSTANTS.TOOL_FUNCTION,
264
+ function: { name: claudeRequest.tool_choice.name }
265
+ };
266
+ } else {
267
+ payload.tool_choice = "auto";
268
+ }
269
+ }
270
+
271
+ return payload;
272
+ }
273
+
274
+ function convertOpenAIToClaudeResponse(openaiResponse, originalRequest) {
275
+ const choice = (openaiResponse.choices || [])[0];
276
+ if (!choice) {
277
+ throw new Error("No choices returned by upstream API.");
278
+ }
279
+
280
+ const message = choice.message || {};
281
+ const content = [];
282
+
283
+ if (message.content != null) {
284
+ content.push({ type: CONSTANTS.CONTENT_TEXT, text: message.content });
285
+ }
286
+
287
+ for (const toolCall of message.tool_calls || []) {
288
+ if (toolCall.type !== CONSTANTS.TOOL_FUNCTION) {
289
+ continue;
290
+ }
291
+ let parsedArguments;
292
+ try {
293
+ parsedArguments = JSON.parse(toolCall.function?.arguments || "{}");
294
+ } catch {
295
+ parsedArguments = { raw_arguments: toolCall.function?.arguments || "" };
296
+ }
297
+
298
+ content.push({
299
+ type: CONSTANTS.CONTENT_TOOL_USE,
300
+ id: toolCall.id || randomMessageId("tool"),
301
+ name: toolCall.function?.name || "",
302
+ input: parsedArguments
303
+ });
304
+ }
305
+
306
+ if (content.length === 0) {
307
+ content.push({ type: CONSTANTS.CONTENT_TEXT, text: "" });
308
+ }
309
+
310
+ const finishReason = choice.finish_reason || "stop";
311
+ const stopReasonMap = {
312
+ stop: CONSTANTS.STOP_END_TURN,
313
+ length: CONSTANTS.STOP_MAX_TOKENS,
314
+ tool_calls: CONSTANTS.STOP_TOOL_USE,
315
+ function_call: CONSTANTS.STOP_TOOL_USE
316
+ };
317
+
318
+ return {
319
+ id: openaiResponse.id || randomMessageId(),
320
+ type: "message",
321
+ role: CONSTANTS.ROLE_ASSISTANT,
322
+ model: originalRequest.model,
323
+ content,
324
+ stop_reason: stopReasonMap[finishReason] || CONSTANTS.STOP_END_TURN,
325
+ stop_sequence: null,
326
+ usage: {
327
+ input_tokens: openaiResponse.usage?.prompt_tokens || 0,
328
+ output_tokens: openaiResponse.usage?.completion_tokens || 0
329
+ }
330
+ };
331
+ }
332
+
333
+ function writeSseEvent(response, event, data) {
334
+ response.write(`event: ${event}\n`);
335
+ response.write(`data: ${JSON.stringify(data)}\n\n`);
336
+ }
337
+
338
+ async function streamOpenAiToClaude(upstreamResponse, response, originalRequest, requestAbortController) {
339
+ const messageId = randomMessageId();
340
+ const toolCalls = new Map();
341
+ let toolBlockCounter = 0;
342
+ let finalStopReason = CONSTANTS.STOP_END_TURN;
343
+ let usage = { input_tokens: 0, output_tokens: 0 };
344
+
345
+ writeSseEvent(response, CONSTANTS.EVENT_MESSAGE_START, {
346
+ type: CONSTANTS.EVENT_MESSAGE_START,
347
+ message: {
348
+ id: messageId,
349
+ type: "message",
350
+ role: CONSTANTS.ROLE_ASSISTANT,
351
+ model: originalRequest.model,
352
+ content: [],
353
+ stop_reason: null,
354
+ stop_sequence: null,
355
+ usage
356
+ }
357
+ });
358
+
359
+ writeSseEvent(response, CONSTANTS.EVENT_CONTENT_BLOCK_START, {
360
+ type: CONSTANTS.EVENT_CONTENT_BLOCK_START,
361
+ index: 0,
362
+ content_block: {
363
+ type: CONSTANTS.CONTENT_TEXT,
364
+ text: ""
365
+ }
366
+ });
367
+
368
+ writeSseEvent(response, CONSTANTS.EVENT_PING, {
369
+ type: CONSTANTS.EVENT_PING
370
+ });
371
+
372
+ const decoder = new TextDecoder();
373
+ const reader = upstreamResponse.body.getReader();
374
+ let buffer = "";
375
+
376
+ try {
377
+ while (true) {
378
+ const { done, value } = await reader.read();
379
+ if (done) {
380
+ break;
381
+ }
382
+
383
+ buffer += decoder.decode(value, { stream: true });
384
+ const lines = buffer.split(/\r?\n/);
385
+ buffer = lines.pop() || "";
386
+
387
+ for (const line of lines) {
388
+ if (!line.startsWith("data: ")) {
389
+ continue;
390
+ }
391
+
392
+ const chunkData = line.slice(6);
393
+ if (chunkData === "[DONE]") {
394
+ break;
395
+ }
396
+
397
+ let chunk;
398
+ try {
399
+ chunk = JSON.parse(chunkData);
400
+ } catch {
401
+ continue;
402
+ }
403
+
404
+ if (chunk.usage) {
405
+ usage = {
406
+ input_tokens: chunk.usage.prompt_tokens || usage.input_tokens || 0,
407
+ output_tokens: chunk.usage.completion_tokens || usage.output_tokens || 0
408
+ };
409
+ }
410
+
411
+ const choice = (chunk.choices || [])[0];
412
+ if (!choice) {
413
+ continue;
414
+ }
415
+
416
+ const delta = choice.delta || {};
417
+
418
+ if (typeof delta.content === "string" && delta.content.length > 0) {
419
+ writeSseEvent(response, CONSTANTS.EVENT_CONTENT_BLOCK_DELTA, {
420
+ type: CONSTANTS.EVENT_CONTENT_BLOCK_DELTA,
421
+ index: 0,
422
+ delta: {
423
+ type: CONSTANTS.DELTA_TEXT,
424
+ text: delta.content
425
+ }
426
+ });
427
+ }
428
+
429
+ for (const toolDelta of delta.tool_calls || []) {
430
+ const index = toolDelta.index || 0;
431
+ if (!toolCalls.has(index)) {
432
+ toolCalls.set(index, {
433
+ id: null,
434
+ name: null,
435
+ started: false,
436
+ claudeIndex: null
437
+ });
438
+ }
439
+
440
+ const tool = toolCalls.get(index);
441
+ if (toolDelta.id) {
442
+ tool.id = toolDelta.id;
443
+ }
444
+ if (toolDelta.function?.name) {
445
+ tool.name = toolDelta.function.name;
446
+ }
447
+
448
+ if (!tool.started && tool.id && tool.name) {
449
+ toolBlockCounter += 1;
450
+ tool.started = true;
451
+ tool.claudeIndex = toolBlockCounter;
452
+ writeSseEvent(response, CONSTANTS.EVENT_CONTENT_BLOCK_START, {
453
+ type: CONSTANTS.EVENT_CONTENT_BLOCK_START,
454
+ index: tool.claudeIndex,
455
+ content_block: {
456
+ type: CONSTANTS.CONTENT_TOOL_USE,
457
+ id: tool.id,
458
+ name: tool.name,
459
+ input: {}
460
+ }
461
+ });
462
+ }
463
+
464
+ if (tool.started && typeof toolDelta.function?.arguments === "string" && toolDelta.function.arguments) {
465
+ writeSseEvent(response, CONSTANTS.EVENT_CONTENT_BLOCK_DELTA, {
466
+ type: CONSTANTS.EVENT_CONTENT_BLOCK_DELTA,
467
+ index: tool.claudeIndex,
468
+ delta: {
469
+ type: CONSTANTS.DELTA_INPUT_JSON,
470
+ partial_json: toolDelta.function.arguments
471
+ }
472
+ });
473
+ }
474
+ }
475
+
476
+ if (choice.finish_reason) {
477
+ if (choice.finish_reason === "length") {
478
+ finalStopReason = CONSTANTS.STOP_MAX_TOKENS;
479
+ } else if (["tool_calls", "function_call"].includes(choice.finish_reason)) {
480
+ finalStopReason = CONSTANTS.STOP_TOOL_USE;
481
+ } else {
482
+ finalStopReason = CONSTANTS.STOP_END_TURN;
483
+ }
484
+ }
485
+ }
486
+ }
487
+ } catch (error) {
488
+ requestAbortController.abort();
489
+ writeSseEvent(response, "error", {
490
+ type: "error",
491
+ error: {
492
+ type: "api_error",
493
+ message: error.message
494
+ }
495
+ });
496
+ return;
497
+ }
498
+
499
+ writeSseEvent(response, CONSTANTS.EVENT_CONTENT_BLOCK_STOP, {
500
+ type: CONSTANTS.EVENT_CONTENT_BLOCK_STOP,
501
+ index: 0
502
+ });
503
+
504
+ for (const tool of toolCalls.values()) {
505
+ if (tool.started) {
506
+ writeSseEvent(response, CONSTANTS.EVENT_CONTENT_BLOCK_STOP, {
507
+ type: CONSTANTS.EVENT_CONTENT_BLOCK_STOP,
508
+ index: tool.claudeIndex
509
+ });
510
+ }
511
+ }
512
+
513
+ writeSseEvent(response, CONSTANTS.EVENT_MESSAGE_DELTA, {
514
+ type: CONSTANTS.EVENT_MESSAGE_DELTA,
515
+ delta: {
516
+ stop_reason: finalStopReason,
517
+ stop_sequence: null
518
+ },
519
+ usage
520
+ });
521
+
522
+ writeSseEvent(response, CONSTANTS.EVENT_MESSAGE_STOP, {
523
+ type: CONSTANTS.EVENT_MESSAGE_STOP
524
+ });
525
+ }
526
+
527
+ module.exports = {
528
+ CONSTANTS,
529
+ convertClaudeToOpenAI,
530
+ convertOpenAIToClaudeResponse,
531
+ mapClaudeModelToOpenAI,
532
+ streamOpenAiToClaude
533
+ };