File size: 5,923 Bytes
5844451
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/**

 * Cursor2API v2 - ๅ…ฅๅฃ

 *

 * ๅฐ† Cursor ๆ–‡ๆกฃ้กตๅ…่ดน AI ๆŽฅๅฃไปฃ็†ไธบ Anthropic Messages API

 * ้€š่ฟ‡ๆ็คบ่ฏๆณจๅ…ฅ่ฎฉ Claude Code ๆ‹ฅๆœ‰ๅฎŒๆ•ดๅทฅๅ…ท่ฐƒ็”จ่ƒฝๅŠ›

 */

import 'dotenv/config';
import { createRequire } from 'module';
import express from 'express';
import { getConfig } from './config.js';
import { handleMessages, listModels, countTokens } from './handler.js';
import { handleOpenAIChatCompletions, handleOpenAIResponses } from './openai-handler.js';

// ไปŽ package.json ่ฏปๅ–็‰ˆๆœฌๅท๏ผŒ็ปŸไธ€ๆฅๆบ๏ผŒ้ฟๅ…ๅคšๅค„็กฌ็ผ–็ 
const require = createRequire(import.meta.url);
const { version: VERSION } = require('../package.json') as { version: string };


const app = express();
const config = getConfig();

// ่งฃๆž JSON body๏ผˆๅขžๅคง้™ๅˆถไปฅๆ”ฏๆŒ base64 ๅ›พ็‰‡๏ผŒๅ•ๅผ ๅ›พ็‰‡ๅฏ่พพ 10MB+๏ผ‰
app.use(express.json({ limit: '50mb' }));

// CORS
app.use((_req, res, next) => {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    res.header('Access-Control-Allow-Headers', '*');
    if (_req.method === 'OPTIONS') {
        res.sendStatus(200);
        return;
    }
    next();
});

// Optional API Key auth (recommended for public deployments)
// - Enabled only when API_KEY is set
// - Accepts either:
//   1) x-api-key: <API_KEY>              (Anthropic-style)
//   2) Authorization: Bearer <API_KEY>   (OpenAI-style)
const API_KEY = (process.env.API_KEY || '').trim();
if (API_KEY) {
    app.use((req, res, next) => {
        // Public endpoints
        if (req.path === '/' || req.path === '/health') return next();

        const headerKey = (req.header('x-api-key') || '').trim();
        const auth = (req.header('authorization') || '').trim();

        let provided = headerKey;
        if (!provided && auth.toLowerCase().startsWith('bearer ')) {
            provided = auth.slice('bearer '.length).trim();
        }

        if (provided && provided === API_KEY) return next();

        res.status(401).json({ error: { message: 'Unauthorized', type: 'auth_error' } });
    });
}

// ==================== ่ทฏ็”ฑ ====================

// Anthropic Messages API
app.post('/v1/messages', handleMessages);
app.post('/messages', handleMessages);

// OpenAI Chat Completions API๏ผˆๅ…ผๅฎน๏ผ‰
app.post('/v1/chat/completions', handleOpenAIChatCompletions);
app.post('/chat/completions', handleOpenAIChatCompletions);

// OpenAI Responses API๏ผˆCursor IDE Agent ๆจกๅผ๏ผ‰
app.post('/v1/responses', handleOpenAIResponses);
app.post('/responses', handleOpenAIResponses);

// Token ่ฎกๆ•ฐ
app.post('/v1/messages/count_tokens', countTokens);
app.post('/messages/count_tokens', countTokens);

// OpenAI ๅ…ผๅฎนๆจกๅž‹ๅˆ—่กจ
app.get('/v1/models', listModels);

// ๅฅๅบทๆฃ€ๆŸฅ
app.get('/health', (_req, res) => {
    res.json({ status: 'ok', version: VERSION });
});

// ๆ น่ทฏๅพ„
app.get('/', (_req, res) => {
    res.json({
        name: 'cursor2api',
        version: VERSION,
        description: 'Cursor Docs AI โ†’ Anthropic & OpenAI & Cursor IDE API Proxy',
        endpoints: {
            anthropic_messages: 'POST /v1/messages',
            openai_chat: 'POST /v1/chat/completions',
            openai_responses: 'POST /v1/responses',
            models: 'GET /v1/models',
            health: 'GET /health',
        },
        auth: API_KEY
            ? { required: true, headers: ['x-api-key', 'Authorization: Bearer'] }
            : { required: false },
        usage: {
            claude_code: 'export ANTHROPIC_BASE_URL=http://localhost:' + config.port,
            openai_compatible: 'OPENAI_BASE_URL=http://localhost:' + config.port + '/v1',
            cursor_ide: 'OPENAI_BASE_URL=http://localhost:' + config.port + '/v1 (้€‰็”จ Claude ๆจกๅž‹)',
        },
    });
});

// ==================== ๅฏๅŠจ ====================

const server = app.listen(config.port, '0.0.0.0', () => {
    console.log('');
    console.log('  โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—');
    console.log(`  โ•‘        Cursor2API v${VERSION.padEnd(21)}โ•‘`);
    console.log('  โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ');
    console.log(`  โ•‘  Server:  http://localhost:${config.port}      โ•‘`);
    console.log('  โ•‘  Model:   ' + config.cursorModel.padEnd(26) + 'โ•‘');
    console.log('  โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ');
    console.log('  โ•‘  API Endpoints:                      โ•‘');
    console.log('  โ•‘  โ€ข Anthropic: /v1/messages            โ•‘');
    console.log('  โ•‘  โ€ข OpenAI:   /v1/chat/completions     โ•‘');
    console.log('  โ•‘  โ€ข Cursor:   /v1/responses            โ•‘');
    console.log('  โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ');
    console.log('  โ•‘  Claude Code:                        โ•‘');
    console.log(`  โ•‘  export ANTHROPIC_BASE_URL=           โ•‘`);
    console.log(`  โ•‘    http://localhost:${config.port}              โ•‘`);
    console.log('  โ•‘  OpenAI / Cursor IDE:                 โ•‘');
    console.log(`  โ•‘  OPENAI_BASE_URL=                     โ•‘`);
    console.log(`  โ•‘    http://localhost:${config.port}/v1            โ•‘`);
    console.log('  โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•');
    console.log('');
});

// ่งฃ้™ค Node.js HTTP Server ็š„้ป˜่ฎค่ถ…ๆ—ถ้™ๅˆถ๏ผŒ้˜ฒๆญข้•ฟๆ—ถ AI ๆตๅผ่พ“ๅ‡บ่ขซๆœฌๅœฐๆސๆ–ญ
server.timeout = 0; 
server.keepAliveTimeout = 120 * 1000;
server.headersTimeout = 125 * 1000;