copilot-cursor-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/README.md +226 -0
- package/anthropic-transforms.ts +185 -0
- package/bin/cli.js +49 -0
- package/dashboard.html +299 -0
- package/debug-logger.ts +53 -0
- package/package.json +36 -0
- package/proxy-router.ts +148 -0
- package/responses-bridge.ts +119 -0
- package/responses-converters.ts +170 -0
- package/start.ts +138 -0
- package/stream-proxy.ts +50 -0
package/dashboard.html
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Copilot API Dashboard</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg-color: #fcfcfc;
|
|
10
|
+
--text-color: #1a1a1a;
|
|
11
|
+
--border-color: #e0e0e0;
|
|
12
|
+
--accent-gray: #666666;
|
|
13
|
+
--hover-bg: #f5f5f5;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
body {
|
|
17
|
+
font-family: 'Times New Roman', Times, serif; /* Roman style */
|
|
18
|
+
background-color: var(--bg-color);
|
|
19
|
+
color: var(--text-color);
|
|
20
|
+
margin: 0;
|
|
21
|
+
padding: 40px;
|
|
22
|
+
line-height: 1.6;
|
|
23
|
+
max-width: 900px;
|
|
24
|
+
margin-left: auto;
|
|
25
|
+
margin-right: auto;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
h1, h2, h3 {
|
|
29
|
+
font-weight: normal;
|
|
30
|
+
letter-spacing: 0.05em;
|
|
31
|
+
text-transform: uppercase;
|
|
32
|
+
margin-bottom: 1.5rem;
|
|
33
|
+
border-bottom: 1px solid var(--text-color);
|
|
34
|
+
padding-bottom: 10px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
h1 { font-size: 2rem; margin-top: 0; }
|
|
38
|
+
h2 { font-size: 1.2rem; margin-top: 3rem; color: var(--accent-gray); border-bottom: 1px solid var(--border-color); }
|
|
39
|
+
|
|
40
|
+
.container {
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
gap: 20px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Usage Section */
|
|
47
|
+
.usage-grid {
|
|
48
|
+
display: grid;
|
|
49
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
50
|
+
gap: 20px;
|
|
51
|
+
margin-bottom: 20px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.card {
|
|
55
|
+
border: 1px solid var(--border-color);
|
|
56
|
+
padding: 20px;
|
|
57
|
+
background: white;
|
|
58
|
+
transition: all 0.2s ease;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.card:hover {
|
|
62
|
+
border-color: var(--accent-gray);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.label {
|
|
66
|
+
font-size: 0.85rem;
|
|
67
|
+
color: var(--accent-gray);
|
|
68
|
+
text-transform: uppercase;
|
|
69
|
+
letter-spacing: 0.1em;
|
|
70
|
+
margin-bottom: 5px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.value {
|
|
74
|
+
font-size: 1.2rem;
|
|
75
|
+
font-weight: bold;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Models Table */
|
|
79
|
+
table {
|
|
80
|
+
width: 100%;
|
|
81
|
+
border-collapse: collapse;
|
|
82
|
+
font-size: 0.95rem;
|
|
83
|
+
margin-top: 10px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
th {
|
|
87
|
+
text-align: left;
|
|
88
|
+
padding: 15px;
|
|
89
|
+
border-bottom: 2px solid var(--text-color);
|
|
90
|
+
font-weight: normal;
|
|
91
|
+
text-transform: uppercase;
|
|
92
|
+
font-size: 0.8rem;
|
|
93
|
+
letter-spacing: 0.1em;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
td {
|
|
97
|
+
padding: 15px;
|
|
98
|
+
border-bottom: 1px solid var(--border-color);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
tr:hover td {
|
|
102
|
+
background-color: var(--hover-bg);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.model-id {
|
|
106
|
+
font-family: 'Courier New', Courier, monospace; /* Monospace for code */
|
|
107
|
+
background: #f4f4f4;
|
|
108
|
+
padding: 4px 8px;
|
|
109
|
+
border-radius: 2px;
|
|
110
|
+
font-size: 0.9em;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.copy-btn {
|
|
114
|
+
background: transparent;
|
|
115
|
+
border: 1px solid var(--border-color);
|
|
116
|
+
color: var(--accent-gray);
|
|
117
|
+
cursor: pointer;
|
|
118
|
+
padding: 4px 10px;
|
|
119
|
+
font-family: inherit;
|
|
120
|
+
font-size: 0.8rem;
|
|
121
|
+
text-transform: uppercase;
|
|
122
|
+
margin-left: 10px;
|
|
123
|
+
transition: all 0.2s;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.copy-btn:hover {
|
|
127
|
+
background: var(--text-color);
|
|
128
|
+
color: white;
|
|
129
|
+
border-color: var(--text-color);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.copy-btn.copied {
|
|
133
|
+
background: var(--text-color);
|
|
134
|
+
color: white;
|
|
135
|
+
border-color: var(--text-color);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#loading {
|
|
139
|
+
text-align: center;
|
|
140
|
+
color: var(--accent-gray);
|
|
141
|
+
font-style: italic;
|
|
142
|
+
padding: 40px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.error {
|
|
146
|
+
color: #333;
|
|
147
|
+
border: 1px solid #333;
|
|
148
|
+
padding: 20px;
|
|
149
|
+
text-align: center;
|
|
150
|
+
}
|
|
151
|
+
</style>
|
|
152
|
+
</head>
|
|
153
|
+
<body>
|
|
154
|
+
|
|
155
|
+
<h1>Copilot API Dashboard</h1>
|
|
156
|
+
|
|
157
|
+
<div id="loading">Fetching data from Proxy...</div>
|
|
158
|
+
<div id="error-msg" class="error" style="display: none;"></div>
|
|
159
|
+
|
|
160
|
+
<div id="content" style="display: none;">
|
|
161
|
+
<!-- Usage Section -->
|
|
162
|
+
<h2>Account & Quota</h2>
|
|
163
|
+
<div class="usage-grid">
|
|
164
|
+
<div class="card">
|
|
165
|
+
<div class="label">User</div>
|
|
166
|
+
<div class="value" id="user-login">-</div>
|
|
167
|
+
</div>
|
|
168
|
+
<div class="card">
|
|
169
|
+
<div class="label">Plan</div>
|
|
170
|
+
<div class="value" id="plan-type">-</div>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="card">
|
|
173
|
+
<div class="label">Chat Quota</div>
|
|
174
|
+
<div class="value" id="chat-quota">-</div>
|
|
175
|
+
</div>
|
|
176
|
+
<div class="card">
|
|
177
|
+
<div class="label">Reset Date</div>
|
|
178
|
+
<div class="value" id="reset-date">-</div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<!-- Models Section -->
|
|
183
|
+
<h2>Available Models <span style="font-size: 0.8rem; float: right; margin-top: 5px; color: var(--accent-gray);" id="model-count"></span></h2>
|
|
184
|
+
<p style="font-size: 0.9rem; color: #666; margin-bottom: 20px;">
|
|
185
|
+
ℹ️ Use the <strong>Cursor Model ID</strong> when configuring Cursor to bypass internal routing.
|
|
186
|
+
</p>
|
|
187
|
+
|
|
188
|
+
<table id="models-table">
|
|
189
|
+
<thead>
|
|
190
|
+
<tr>
|
|
191
|
+
<th>Model Name</th>
|
|
192
|
+
<th>Original ID</th>
|
|
193
|
+
<th>Cursor Model ID (Use This)</th>
|
|
194
|
+
</tr>
|
|
195
|
+
</thead>
|
|
196
|
+
<tbody id="models-list">
|
|
197
|
+
<!-- Rows injected here -->
|
|
198
|
+
</tbody>
|
|
199
|
+
</table>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<script>
|
|
203
|
+
// Since dashboard is served by the proxy server itself, use relative paths!
|
|
204
|
+
const API_BASE = '';
|
|
205
|
+
|
|
206
|
+
async function fetchData() {
|
|
207
|
+
try {
|
|
208
|
+
// Fetch Usage (Proxy handles forwarding)
|
|
209
|
+
const usageRes = await fetch(`${API_BASE}/usage`);
|
|
210
|
+
if (!usageRes.ok) throw new Error('Failed to fetch usage');
|
|
211
|
+
const usageData = await usageRes.json();
|
|
212
|
+
|
|
213
|
+
// Fetch Models (Proxy handles modification)
|
|
214
|
+
const modelsRes = await fetch(`${API_BASE}/v1/models`);
|
|
215
|
+
if (!modelsRes.ok) throw new Error('Failed to fetch models');
|
|
216
|
+
const modelsData = await modelsRes.json();
|
|
217
|
+
|
|
218
|
+
render(usageData, modelsData);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
showError(err.message);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function render(usage, models) {
|
|
225
|
+
document.getElementById('loading').style.display = 'none';
|
|
226
|
+
document.getElementById('content').style.display = 'block';
|
|
227
|
+
|
|
228
|
+
// 1. Render Usage
|
|
229
|
+
document.getElementById('user-login').textContent = usage.login || 'Unknown';
|
|
230
|
+
document.getElementById('plan-type').textContent = (usage.copilot_plan || 'free').toUpperCase();
|
|
231
|
+
|
|
232
|
+
// Check quota status
|
|
233
|
+
const chatSnapshot = usage.quota_snapshots?.chat;
|
|
234
|
+
if (chatSnapshot?.unlimited) {
|
|
235
|
+
document.getElementById('chat-quota').textContent = 'UNLIMITED';
|
|
236
|
+
} else {
|
|
237
|
+
document.getElementById('chat-quota').textContent = chatSnapshot?.remaining ?? '0';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Date
|
|
241
|
+
const date = new Date(usage.quota_reset_date_utc || Date.now());
|
|
242
|
+
document.getElementById('reset-date').textContent = date.toLocaleDateString();
|
|
243
|
+
|
|
244
|
+
// 2. Render Models
|
|
245
|
+
const tbody = document.getElementById('models-list');
|
|
246
|
+
const modelList = models.data || [];
|
|
247
|
+
document.getElementById('model-count').textContent = `${modelList.length} MODELS FOUND`;
|
|
248
|
+
|
|
249
|
+
// Sort models alphabetically by ID
|
|
250
|
+
modelList.sort((a, b) => a.id.localeCompare(b.id));
|
|
251
|
+
|
|
252
|
+
modelList.forEach(m => {
|
|
253
|
+
// The proxy already adds the prefix to 'id' and 'display_name'
|
|
254
|
+
const prefix = "cus-";
|
|
255
|
+
const originalId = m.id.startsWith(prefix) ? m.id.slice(prefix.length) : m.id;
|
|
256
|
+
|
|
257
|
+
const tr = document.createElement('tr');
|
|
258
|
+
tr.innerHTML = `
|
|
259
|
+
<td style="font-style: italic;">${m.display_name || m.id}</td>
|
|
260
|
+
<td><span class="model-id" style="color: #888;">${originalId}</span></td>
|
|
261
|
+
<td>
|
|
262
|
+
<span class="model-id" style="font-weight: bold;">${m.id}</span>
|
|
263
|
+
<button class="copy-btn" onclick="copyToClipboard('${m.id}', this)">Copy</button>
|
|
264
|
+
</td>
|
|
265
|
+
`;
|
|
266
|
+
tbody.appendChild(tr);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function copyToClipboard(text, btn) {
|
|
271
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
272
|
+
const originalText = btn.textContent;
|
|
273
|
+
btn.textContent = 'Copied';
|
|
274
|
+
btn.classList.add('copied');
|
|
275
|
+
setTimeout(() => {
|
|
276
|
+
btn.textContent = originalText;
|
|
277
|
+
btn.classList.remove('copied');
|
|
278
|
+
}, 1500);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function showError(msg) {
|
|
283
|
+
document.getElementById('loading').style.display = 'none';
|
|
284
|
+
const errDiv = document.getElementById('error-msg');
|
|
285
|
+
errDiv.style.display = 'block';
|
|
286
|
+
errDiv.innerHTML = `
|
|
287
|
+
<strong>Connection Error</strong><br>
|
|
288
|
+
${msg}<br><br>
|
|
289
|
+
Ensure both services are running:<br>
|
|
290
|
+
1. <code>npx copilot-api start</code> (Port 4141)<br>
|
|
291
|
+
2. <code>bun run proxy-router.ts</code> (Port 4142)
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Start
|
|
296
|
+
fetchData();
|
|
297
|
+
</script>
|
|
298
|
+
</body>
|
|
299
|
+
</html>
|
package/debug-logger.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const logIncomingRequest = (json: any): void => {
|
|
2
|
+
console.log('\n' + '='.repeat(80));
|
|
3
|
+
console.log('📥 INCOMING REQUEST:', new Date().toISOString());
|
|
4
|
+
console.log('📥 ALL KEYS:', Object.keys(json).join(', '));
|
|
5
|
+
console.log('📥 Model:', json.model);
|
|
6
|
+
console.log('📥 Stream:', json.stream);
|
|
7
|
+
console.log('📥 tool_choice (raw):', JSON.stringify(json.tool_choice));
|
|
8
|
+
console.log('📥 # tools:', json.tools?.length ?? 0);
|
|
9
|
+
console.log('📥 # messages:', json.messages?.length ?? 0);
|
|
10
|
+
if (json.input !== undefined) console.log('📥 ⚠️ INPUT FIELD (Responses API):', JSON.stringify(json.input).slice(0, 1000));
|
|
11
|
+
if (json.instructions !== undefined) console.log('📥 ⚠️ INSTRUCTIONS FIELD:', JSON.stringify(json.instructions).slice(0, 500));
|
|
12
|
+
if (json.system) console.log('📥 ⚠️ TOP-LEVEL system FIELD DETECTED:', JSON.stringify(json.system).slice(0, 500));
|
|
13
|
+
if (json.max_tokens) console.log('📥 max_tokens:', json.max_tokens);
|
|
14
|
+
if (json.metadata) console.log('📥 ⚠️ metadata:', JSON.stringify(json.metadata).slice(0, 300));
|
|
15
|
+
if (json.stop_sequences) console.log('📥 ⚠️ stop_sequences:', JSON.stringify(json.stop_sequences));
|
|
16
|
+
if (json.messages) {
|
|
17
|
+
const last3 = json.messages.slice(-3);
|
|
18
|
+
for (const m of last3) {
|
|
19
|
+
const contentPreview = typeof m.content === 'string'
|
|
20
|
+
? m.content.slice(0, 200)
|
|
21
|
+
: JSON.stringify(m.content).slice(0, 500);
|
|
22
|
+
console.log(`📥 MSG [${m.role}]: ${contentPreview}`);
|
|
23
|
+
if (Array.isArray(m.content)) {
|
|
24
|
+
const types = m.content.map((c: any) => c.type).join(', ');
|
|
25
|
+
console.log(`📥 Content types: [${types}]`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (json.tool_choice && typeof json.tool_choice === 'object') {
|
|
30
|
+
console.log('📥 ⚠️ tool_choice is OBJECT:', JSON.stringify(json.tool_choice));
|
|
31
|
+
}
|
|
32
|
+
console.log('='.repeat(80));
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const logTransformedRequest = (json: any): void => {
|
|
36
|
+
console.log('\n' + '-'.repeat(80));
|
|
37
|
+
console.log('📤 TRANSFORMED REQUEST:');
|
|
38
|
+
console.log('📤 Model:', json.model);
|
|
39
|
+
console.log('📤 tool_choice (after):', JSON.stringify(json.tool_choice));
|
|
40
|
+
console.log('📤 # tools:', json.tools?.length ?? 0);
|
|
41
|
+
console.log('📤 # messages:', json.messages?.length ?? 0);
|
|
42
|
+
if (json.messages) {
|
|
43
|
+
const last3 = json.messages.slice(-3);
|
|
44
|
+
for (const m of last3) {
|
|
45
|
+
const contentPreview = typeof m.content === 'string'
|
|
46
|
+
? m.content.slice(0, 200)
|
|
47
|
+
: JSON.stringify(m.content).slice(0, 500);
|
|
48
|
+
console.log(`📤 MSG [${m.role}]: ${contentPreview}`);
|
|
49
|
+
if (m.tool_calls) console.log(`📤 tool_calls: ${JSON.stringify(m.tool_calls).slice(0, 300)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
console.log('-'.repeat(80));
|
|
53
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "copilot-cursor-proxy",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Proxy that bridges GitHub Copilot API to Cursor IDE — translates Anthropic format, bridges Responses API for GPT 5.x, and more",
|
|
5
|
+
"bin": {
|
|
6
|
+
"copilot-cursor-proxy": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"*.ts",
|
|
11
|
+
"dashboard.html",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "bun build start.ts proxy-router.ts anthropic-transforms.ts responses-bridge.ts responses-converters.ts stream-proxy.ts debug-logger.ts --outdir dist --target node",
|
|
16
|
+
"dev": "bun run start.ts",
|
|
17
|
+
"start": "node dist/start.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"copilot",
|
|
21
|
+
"cursor",
|
|
22
|
+
"proxy",
|
|
23
|
+
"anthropic",
|
|
24
|
+
"openai",
|
|
25
|
+
"responses-api"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/CharlesYWL/copilot-for-cursor.git"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18",
|
|
34
|
+
"bun": ">=1.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/proxy-router.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { normalizeRequest } from './anthropic-transforms';
|
|
2
|
+
import { handleResponsesAPIBridge } from './responses-bridge';
|
|
3
|
+
import { createStreamProxy } from './stream-proxy';
|
|
4
|
+
import { logIncomingRequest, logTransformedRequest } from './debug-logger';
|
|
5
|
+
|
|
6
|
+
const PORT = 4142;
|
|
7
|
+
const TARGET_URL = "http://localhost:4141";
|
|
8
|
+
const PREFIX = "cus-";
|
|
9
|
+
let responseCounter = 0;
|
|
10
|
+
|
|
11
|
+
console.log(`🚀 Proxy Router running on http://localhost:${PORT}`);
|
|
12
|
+
console.log(`🔗 Forwarding to ${TARGET_URL}`);
|
|
13
|
+
console.log(`🏷️ Prefix: "${PREFIX}"`);
|
|
14
|
+
|
|
15
|
+
Bun.serve({
|
|
16
|
+
port: PORT,
|
|
17
|
+
idleTimeout: 255,
|
|
18
|
+
async fetch(req) {
|
|
19
|
+
const url = new URL(req.url);
|
|
20
|
+
|
|
21
|
+
if (url.pathname === "/" || url.pathname === "/dashboard.html") {
|
|
22
|
+
try {
|
|
23
|
+
const dashboardContent = await Bun.file("dashboard.html").text();
|
|
24
|
+
return new Response(dashboardContent, { headers: { "Content-Type": "text/html" } });
|
|
25
|
+
} catch (e) {
|
|
26
|
+
return new Response("Dashboard not found.", { status: 404 });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const targetUrl = new URL(url.pathname + url.search, TARGET_URL);
|
|
31
|
+
|
|
32
|
+
if (req.method === "OPTIONS") {
|
|
33
|
+
return new Response(null, {
|
|
34
|
+
headers: {
|
|
35
|
+
"Access-Control-Allow-Origin": "*",
|
|
36
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
37
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
if (req.method === "POST" && url.pathname.includes("/chat/completions")) {
|
|
44
|
+
let json = await req.json();
|
|
45
|
+
|
|
46
|
+
logIncomingRequest(json);
|
|
47
|
+
|
|
48
|
+
const originalModel = json.model;
|
|
49
|
+
let targetModel = json.model;
|
|
50
|
+
|
|
51
|
+
if (json.model && json.model.startsWith(PREFIX)) {
|
|
52
|
+
targetModel = json.model.slice(PREFIX.length);
|
|
53
|
+
json.model = targetModel;
|
|
54
|
+
console.log(`🔄 Rewriting model: ${originalModel} -> ${json.model}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const isClaude = targetModel.toLowerCase().includes('claude');
|
|
58
|
+
|
|
59
|
+
normalizeRequest(json, isClaude);
|
|
60
|
+
|
|
61
|
+
logTransformedRequest(json);
|
|
62
|
+
|
|
63
|
+
const body = JSON.stringify(json);
|
|
64
|
+
const headers = new Headers(req.headers);
|
|
65
|
+
headers.set("host", targetUrl.host);
|
|
66
|
+
headers.set("content-length", String(new TextEncoder().encode(body).length));
|
|
67
|
+
|
|
68
|
+
const needsResponsesAPI = targetModel.match(/^gpt-5\.[2-9]|^gpt-5\.\d+-codex|^o[1-9]|^goldeneye/i);
|
|
69
|
+
|
|
70
|
+
if (needsResponsesAPI) {
|
|
71
|
+
console.log(`🔀 Model ${targetModel} requires Responses API — converting`);
|
|
72
|
+
const chatId = `chatcmpl-proxy-${++responseCounter}`;
|
|
73
|
+
return await handleResponsesAPIBridge(json, req, chatId, TARGET_URL);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const hasVisionContent = (messages: any[]) => messages?.some(msg =>
|
|
77
|
+
Array.isArray(msg.content) && msg.content.some((p: any) => p.type === 'image_url')
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (!isClaude && json.messages && hasVisionContent(json.messages)) {
|
|
81
|
+
headers.set("Copilot-Vision-Request", "true");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const response = await fetch(targetUrl.toString(), {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: headers,
|
|
87
|
+
body: body,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const responseHeaders = new Headers(response.headers);
|
|
91
|
+
responseHeaders.set("Access-Control-Allow-Origin", "*");
|
|
92
|
+
console.log(`📡 Upstream response: ${response.status} | content-type: ${response.headers.get('content-type')}`);
|
|
93
|
+
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
const errText = await response.text();
|
|
96
|
+
console.error(`❌ Upstream Error (${response.status}):`, errText);
|
|
97
|
+
return new Response(errText, { status: response.status, headers: responseHeaders });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (json.stream && response.body) {
|
|
101
|
+
return createStreamProxy(response.body, responseHeaders);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return new Response(response.body, {
|
|
105
|
+
status: response.status,
|
|
106
|
+
headers: responseHeaders,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (req.method === "GET" && url.pathname.includes("/models")) {
|
|
111
|
+
const headers = new Headers(req.headers);
|
|
112
|
+
headers.set("host", targetUrl.host);
|
|
113
|
+
const response = await fetch(targetUrl.toString(), { method: "GET", headers: headers });
|
|
114
|
+
const data = await response.json();
|
|
115
|
+
|
|
116
|
+
if (data.data && Array.isArray(data.data)) {
|
|
117
|
+
data.data = data.data.map((model: any) => ({
|
|
118
|
+
...model,
|
|
119
|
+
id: PREFIX + model.id,
|
|
120
|
+
display_name: PREFIX + (model.display_name || model.id)
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
return new Response(JSON.stringify(data), {
|
|
124
|
+
status: response.status,
|
|
125
|
+
headers: { ...Object.fromEntries(response.headers), "Access-Control-Allow-Origin": "*" }
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const headers = new Headers(req.headers);
|
|
130
|
+
headers.set("host", targetUrl.host);
|
|
131
|
+
const response = await fetch(targetUrl.toString(), {
|
|
132
|
+
method: req.method,
|
|
133
|
+
headers: headers,
|
|
134
|
+
body: req.body,
|
|
135
|
+
});
|
|
136
|
+
const responseHeaders = new Headers(response.headers);
|
|
137
|
+
responseHeaders.set("Access-Control-Allow-Origin", "*");
|
|
138
|
+
return new Response(response.body, { status: response.status, headers: responseHeaders });
|
|
139
|
+
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error("Proxy Error:", error);
|
|
142
|
+
return new Response(JSON.stringify({ error: "Proxy Error", details: String(error) }), {
|
|
143
|
+
status: 500,
|
|
144
|
+
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { convertResponsesSyncToChatCompletions, convertResponsesStreamToChatCompletions } from './responses-converters';
|
|
2
|
+
|
|
3
|
+
export async function handleResponsesAPIBridge(json: any, req: Request, chatId: string, targetUrl: string) {
|
|
4
|
+
const corsHeaders = { "Access-Control-Allow-Origin": "*" };
|
|
5
|
+
|
|
6
|
+
const responsesReq: any = {
|
|
7
|
+
model: json.model,
|
|
8
|
+
stream: json.stream ?? false,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const systemMsgs = (json.messages || []).filter((m: any) => m.role === 'system');
|
|
12
|
+
const nonSystemMsgs = (json.messages || []).filter((m: any) => m.role !== 'system');
|
|
13
|
+
if (systemMsgs.length > 0) {
|
|
14
|
+
responsesReq.instructions = systemMsgs.map((m: any) =>
|
|
15
|
+
typeof m.content === 'string' ? m.content : JSON.stringify(m.content)
|
|
16
|
+
).join('\n');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (json.input !== undefined) {
|
|
20
|
+
responsesReq.input = json.input;
|
|
21
|
+
} else if (nonSystemMsgs.length > 0) {
|
|
22
|
+
responsesReq.input = nonSystemMsgs.map((m: any) => {
|
|
23
|
+
const content = typeof m.content === 'string' ? m.content
|
|
24
|
+
: Array.isArray(m.content) ? m.content.map((p: any) => {
|
|
25
|
+
if (p.type === 'text') return { type: 'input_text', text: p.text };
|
|
26
|
+
if (p.type === 'image_url') return { type: 'input_image', image_url: p.image_url.url };
|
|
27
|
+
return { type: 'input_text', text: JSON.stringify(p) };
|
|
28
|
+
})
|
|
29
|
+
: String(m.content);
|
|
30
|
+
|
|
31
|
+
if (m.role === 'tool') {
|
|
32
|
+
return {
|
|
33
|
+
type: 'function_call_output',
|
|
34
|
+
call_id: m.tool_call_id,
|
|
35
|
+
output: typeof content === 'string' ? content : JSON.stringify(content),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (m.role === 'assistant' && m.tool_calls) {
|
|
40
|
+
const items: any[] = [];
|
|
41
|
+
if (typeof content === 'string' && content) {
|
|
42
|
+
items.push({ role: 'assistant', type: 'message', content: [{ type: 'output_text', text: content }] });
|
|
43
|
+
}
|
|
44
|
+
for (const tc of m.tool_calls) {
|
|
45
|
+
items.push({
|
|
46
|
+
type: 'function_call',
|
|
47
|
+
id: tc.id,
|
|
48
|
+
call_id: tc.id,
|
|
49
|
+
name: tc.function.name,
|
|
50
|
+
arguments: tc.function.arguments,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return items;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
role: m.role === 'assistant' ? 'assistant' : 'user',
|
|
58
|
+
content: typeof content === 'string' ? content : content,
|
|
59
|
+
};
|
|
60
|
+
}).flat();
|
|
61
|
+
} else {
|
|
62
|
+
responsesReq.input = "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (json.tools && Array.isArray(json.tools)) {
|
|
66
|
+
responsesReq.tools = json.tools.map((t: any) => {
|
|
67
|
+
if (t.type === 'function' && t.function) {
|
|
68
|
+
return {
|
|
69
|
+
type: 'function',
|
|
70
|
+
name: t.function.name,
|
|
71
|
+
description: t.function.description,
|
|
72
|
+
parameters: t.function.parameters,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return t;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (json.max_tokens) responsesReq.max_output_tokens = Math.max(json.max_tokens, 16);
|
|
80
|
+
if (json.temperature !== undefined) responsesReq.temperature = json.temperature;
|
|
81
|
+
if (json.top_p !== undefined) responsesReq.top_p = json.top_p;
|
|
82
|
+
|
|
83
|
+
if (json.tool_choice) {
|
|
84
|
+
if (typeof json.tool_choice === 'string') {
|
|
85
|
+
responsesReq.tool_choice = json.tool_choice;
|
|
86
|
+
} else if (json.tool_choice.type === 'function' && json.tool_choice.function) {
|
|
87
|
+
responsesReq.tool_choice = { type: 'function', name: json.tool_choice.function.name };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const responsesBody = JSON.stringify(responsesReq);
|
|
92
|
+
console.log('📤 Responses API request:', responsesBody.slice(0, 500));
|
|
93
|
+
|
|
94
|
+
const responsesUrl = new URL('/v1/responses', targetUrl);
|
|
95
|
+
const headers = new Headers(req.headers);
|
|
96
|
+
headers.set("host", responsesUrl.host);
|
|
97
|
+
headers.set("content-type", "application/json");
|
|
98
|
+
headers.set("content-length", String(new TextEncoder().encode(responsesBody).length));
|
|
99
|
+
|
|
100
|
+
const response = await fetch(responsesUrl.toString(), {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: headers,
|
|
103
|
+
body: responsesBody,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
console.log(`📡 Responses API upstream: ${response.status} | ${response.headers.get('content-type')}`);
|
|
107
|
+
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
const errText = await response.text();
|
|
110
|
+
console.error(`❌ Responses API Error (${response.status}):`, errText);
|
|
111
|
+
return new Response(errText, { status: response.status, headers: corsHeaders });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (json.stream && response.body) {
|
|
115
|
+
return convertResponsesStreamToChatCompletions(response, json.model, chatId, corsHeaders);
|
|
116
|
+
} else {
|
|
117
|
+
return convertResponsesSyncToChatCompletions(response, json.model, chatId, corsHeaders);
|
|
118
|
+
}
|
|
119
|
+
}
|