copilot-cursor-proxy 1.0.4 → 1.1.1

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 CHANGED
@@ -46,12 +46,12 @@ Copy the HTTPS URL (e.g., `https://xxxxx.trycloudflare.com`).
46
46
 
47
47
  ## 🏗 Architecture
48
48
 
49
- ```
49
+ ```text
50
50
  Cursor → (HTTPS tunnel) → proxy-router (:4142) → copilot-api (:4141) → GitHub Copilot
51
51
  ```
52
52
 
53
- * **Port 4141 (`copilot-api`):** Authenticates with GitHub and provides the OpenAI-compatible API.
54
- * *Powered by [copilot-api](https://www.npmjs.com/package/copilot-api) (installed via `npx`).*
53
+ * **Port 4141 (`copilot-api`):** Authenticates with GitHub, provides the OpenAI-compatible API, and natively handles the Responses API for GPT-5.x models.
54
+ * *Powered by [@jeffreycao/copilot-api](https://github.com/caozhiyuan/copilot-api) (installed via `npx`).*
55
55
  * **Port 4142 (`proxy-router`):** Converts Anthropic-format messages to OpenAI format, bridges Responses API for GPT-5.x models, handles the `cus-` prefix, and serves the dashboard.
56
56
  * **HTTPS tunnel:** Cursor requires HTTPS — a tunnel exposes the local proxy.
57
57
 
@@ -81,32 +81,32 @@ Cursor → (HTTPS tunnel) → proxy-router (:4142) → copilot-api (:4141) → G
81
81
 
82
82
  > **💡 Tip:** Visit the [Dashboard](http://localhost:4142) to see all available models and copy their IDs.
83
83
 
84
- ### Tested Models (15/21 passing)
84
+ ### Tested Models (19/20 passing)
85
85
 
86
86
  | Cursor Model Name | Actual Model | Status |
87
87
  |---|---|---|
88
88
  | `cus-gpt-4o` | GPT-4o | ✅ |
89
89
  | `cus-gpt-4.1` | GPT-4.1 | ✅ |
90
+ | `cus-gpt-41-copilot` | GPT-4.1 Copilot | ❌ Not supported by GitHub |
90
91
  | `cus-gpt-5-mini` | GPT-5 Mini | ✅ |
91
- | `cus-gpt-5.1` | GPT-5.1 | ✅ |
92
- | `cus-gpt-5.2` | GPT-5.2 | ⚠️ See note |
93
- | `cus-gpt-5.2-codex` | GPT-5.2 Codex | ⚠️ See note |
94
- | `cus-gpt-5.3-codex` | GPT-5.3 Codex | ⚠️ See note |
95
- | `cus-gpt-5.4` | GPT-5.4 | ⚠️ See note |
96
- | `cus-gpt-5.4-mini` | GPT-5.4 Mini | ⚠️ See note |
97
- | `cus-goldeneye` | Goldeneye | ⚠️ See note |
92
+ | `cus-gpt-5.1` | GPT-5.1 | ✅ (deprecating 2026-04-15) |
93
+ | `cus-gpt-5.2` | GPT-5.2 | |
94
+ | `cus-gpt-5.2-codex` | GPT-5.2 Codex | |
95
+ | `cus-gpt-5.3-codex` | GPT-5.3 Codex | |
96
+ | `cus-gpt-5.4` | GPT-5.4 | |
97
+ | `cus-gpt-5.4-mini` | GPT-5.4 Mini | |
98
98
  | `cus-claude-haiku-4.5` | Claude Haiku 4.5 | ✅ |
99
99
  | `cus-claude-sonnet-4` | Claude Sonnet 4 | ✅ |
100
100
  | `cus-claude-sonnet-4.5` | Claude Sonnet 4.5 | ✅ |
101
101
  | `cus-claude-sonnet-4.6` | Claude Sonnet 4.6 | ✅ |
102
102
  | `cus-claude-opus-4.5` | Claude Opus 4.5 | ✅ |
103
103
  | `cus-claude-opus-4.6` | Claude Opus 4.6 | ✅ |
104
- | `cus-claude-opus-4.6-1m` | Claude Opus 4.6 (1M) | ✅ |
105
104
  | `cus-gemini-2.5-pro` | Gemini 2.5 Pro | ✅ |
106
105
  | `cus-gemini-3-flash-preview` | Gemini 3 Flash | ✅ |
107
106
  | `cus-gemini-3.1-pro-preview` | Gemini 3.1 Pro | ✅ |
107
+ | `cus-text-embedding-3-small` | Text Embedding 3 Small | N/A (embedding model) |
108
108
 
109
- > **⚠️ GPT-5.2+, GPT-5.x-codex, and goldeneye** are currently broken. These models require the `/v1/responses` API or `max_completion_tokens` instead of `max_tokens`, but `copilot-api` injects `max_tokens` into all requests. The proxy has a Responses API bridge built in, but `copilot-api` no longer exposes the `/v1/responses` endpoint. This will be resolved when `copilot-api` is updated. **All Claude, Gemini, GPT-4.x, GPT-5-mini, and GPT-5.1 models work fine.**
109
+ > All GPT-5.x models now work thanks to the switch to [@jeffreycao/copilot-api](https://github.com/caozhiyuan/copilot-api), which natively supports the Responses API. The proxy also includes its own Responses API bridge as a fallback.
110
110
 
111
111
  ![Cursor Settings Configuration](./cursor-settings.png)
112
112
 
@@ -186,7 +186,7 @@ Three tabs:
186
186
  | Streaming | ✅ Works |
187
187
  | Plan mode | ✅ Works |
188
188
  | Agent mode | ✅ Works |
189
- | GPT-5.x models | ⚠️ Blocked by copilot-api `max_tokens` bug |
189
+ | All GPT-5.x models | Works |
190
190
  | Extended thinking (chain-of-thought) | ❌ Stripped |
191
191
  | Prompt caching (`cache_control`) | ❌ Stripped |
192
192
  | Claude Vision | ❌ Not supported via Copilot |
package/auth-config.ts CHANGED
@@ -20,15 +20,19 @@ const CONFIG_PATH = join(CONFIG_DIR, 'auth.json');
20
20
 
21
21
  const DEFAULT_CONFIG: AuthConfig = { requireApiKey: false, keys: [] };
22
22
 
23
+ let cachedConfig: AuthConfig | null = null;
24
+
23
25
  export function loadAuthConfig(): AuthConfig {
26
+ if (cachedConfig) return cachedConfig;
24
27
  try {
25
28
  if (!existsSync(CONFIG_PATH)) return { ...DEFAULT_CONFIG, keys: [] };
26
29
  const raw = readFileSync(CONFIG_PATH, 'utf-8');
27
30
  const parsed = JSON.parse(raw);
28
- return {
31
+ cachedConfig = {
29
32
  requireApiKey: !!parsed.requireApiKey,
30
33
  keys: Array.isArray(parsed.keys) ? parsed.keys : [],
31
34
  };
35
+ return cachedConfig;
32
36
  } catch {
33
37
  return { ...DEFAULT_CONFIG, keys: [] };
34
38
  }
@@ -37,6 +41,7 @@ export function loadAuthConfig(): AuthConfig {
37
41
  export function saveAuthConfig(config: AuthConfig): void {
38
42
  if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
39
43
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
44
+ cachedConfig = null; // Invalidate cache on write
40
45
  }
41
46
 
42
47
  function randomHex(bytes: number): string {
package/dashboard.html CHANGED
@@ -354,10 +354,10 @@ tr:hover td { background: var(--bg-hover); }
354
354
  </div>
355
355
 
356
356
  <!-- Tabs -->
357
- <div class="tabs">
358
- <div class="tab active" data-tab="endpoint">Endpoint</div>
359
- <div class="tab" data-tab="usage">Usage</div>
360
- <div class="tab" data-tab="console">Console Log</div>
357
+ <div class="tabs" role="tablist">
358
+ <div class="tab active" data-tab="endpoint" role="tab" tabindex="0" aria-selected="true">Endpoint</div>
359
+ <div class="tab" data-tab="usage" role="tab" tabindex="0" aria-selected="false">Usage</div>
360
+ <div class="tab" data-tab="console" role="tab" tabindex="0" aria-selected="false">Console Log</div>
361
361
  </div>
362
362
 
363
363
  <!-- Content -->
@@ -537,13 +537,20 @@ tr:hover td { background: var(--bg-hover); }
537
537
  <script>
538
538
  /* ── Tab switching ──────────────────────────────────────────── */
539
539
  const tabs = document.querySelectorAll('.tab');
540
- tabs.forEach(t => t.addEventListener('click', () => {
541
- tabs.forEach(x => x.classList.remove('active'));
540
+ function switchTab(t) {
541
+ tabs.forEach(x => { x.classList.remove('active'); x.setAttribute('aria-selected', 'false'); });
542
542
  document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
543
543
  t.classList.add('active');
544
+ t.setAttribute('aria-selected', 'true');
544
545
  document.getElementById('tab-' + t.dataset.tab).classList.add('active');
545
546
  if (t.dataset.tab === 'usage') fetchUsage();
546
- }));
547
+ }
548
+ tabs.forEach(t => {
549
+ t.addEventListener('click', () => switchTab(t));
550
+ t.addEventListener('keydown', (e) => {
551
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); switchTab(t); }
552
+ });
553
+ });
547
554
 
548
555
  /* ── Helpers ────────────────────────────────────────────────── */
549
556
  function copyText(text, btn) {
@@ -578,7 +585,8 @@ function esc(s) {
578
585
  /* ── Tab 1: Models ──────────────────────────────────────────── */
579
586
  async function fetchModels() {
580
587
  try {
581
- const res = await fetch('/v1/models');
588
+ // Fetch via /api/models to bypass API key auth for dashboard
589
+ const res = await fetch('/api/models');
582
590
  if (!res.ok) throw new Error('HTTP ' + res.status);
583
591
  const data = await res.json();
584
592
  const models = (data.data || []).sort((a, b) => a.id.localeCompare(b.id));
@@ -598,7 +606,9 @@ async function fetchModels() {
598
606
  '<td><span class="model-badge">' + esc(m.id) + '</span></td>' +
599
607
  '<td style="color:var(--text-dim)">' + esc(origId) + '</td>' +
600
608
  '<td style="color:var(--text-dim)">' + esc(m.display_name || m.id) + '</td>' +
601
- '<td><button class="copy-btn" onclick="copyText(\'' + esc(m.id).replace(/'/g, "\\'") + '\',this)">Copy</button></td>';
609
+ '<td><button class="copy-btn">Copy</button></td>';
610
+ const btn = tr.querySelector('.copy-btn');
611
+ btn.addEventListener('click', function() { copyText(m.id, this); });
602
612
  tbody.appendChild(tr);
603
613
  });
604
614
  } catch (e) {
@@ -631,10 +641,13 @@ function renderKeys() {
631
641
  list.innerHTML = '<div style="color: #666; font-size: 13px; padding: 12px 0;">No API keys created yet.</div>';
632
642
  return;
633
643
  }
634
- list.innerHTML = authConfig.keys.map(k => `
635
- <div style="display: flex; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid #222;">
644
+ list.innerHTML = '';
645
+ authConfig.keys.forEach(k => {
646
+ const row = document.createElement('div');
647
+ row.style.cssText = 'display: flex; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid #222;';
648
+ row.innerHTML = `
636
649
  <label class="toggle" style="flex-shrink: 0;">
637
- <input type="checkbox" ${k.active ? 'checked' : ''} onchange="toggleKey('${esc(k.id)}', this.checked)">
650
+ <input type="checkbox" ${k.active ? 'checked' : ''}>
638
651
  <span class="toggle-slider"></span>
639
652
  </label>
640
653
  <div style="flex: 1; min-width: 0;">
@@ -642,9 +655,12 @@ function renderKeys() {
642
655
  <code style="color: #888; font-size: 12px;">${esc(k.key)}</code>
643
656
  </div>
644
657
  <div style="color: #666; font-size: 12px; white-space: nowrap;">${timeAgo(k.createdAt)}</div>
645
- <button onclick="deleteKey('${esc(k.id)}')" style="background: none; border: none; color: #666; cursor: pointer; font-size: 16px; padding: 4px 8px;" title="Delete">🗑</button>
646
- </div>
647
- `).join('');
658
+ <button style="background: none; border: none; color: #666; cursor: pointer; font-size: 16px; padding: 4px 8px;" title="Delete">🗑</button>
659
+ `;
660
+ row.querySelector('input[type="checkbox"]').addEventListener('change', function() { toggleKey(k.id, this.checked); });
661
+ row.querySelector('button[title="Delete"]').addEventListener('click', function() { deleteKey(k.id); });
662
+ list.appendChild(row);
663
+ });
648
664
  }
649
665
 
650
666
  async function toggleRequireKey(enabled) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copilot-cursor-proxy",
3
- "version": "1.0.4",
3
+ "version": "1.1.1",
4
4
  "description": "Proxy that bridges GitHub Copilot API to Cursor IDE — translates Anthropic format, bridges Responses API for GPT 5.x, and more",
5
5
  "bin": {
6
6
  "copilot-cursor-proxy": "bin/cli.js"
@@ -14,7 +14,7 @@
14
14
  "scripts": {
15
15
  "build": "bun build start.ts proxy-router.ts anthropic-transforms.ts responses-bridge.ts responses-converters.ts stream-proxy.ts debug-logger.ts auth-config.ts --outdir dist --target node",
16
16
  "dev": "bun run start.ts",
17
- "start": "node dist/start.js"
17
+ "start": "bun dist/start.js"
18
18
  },
19
19
  "keywords": [
20
20
  "copilot",
package/proxy-router.ts CHANGED
@@ -124,7 +124,19 @@ Bun.serve({
124
124
  }
125
125
 
126
126
  if (url.pathname === "/api/keys" && req.method === "POST") {
127
- const { name } = await req.json();
127
+ let body: unknown;
128
+ try {
129
+ body = await req.json();
130
+ } catch {
131
+ return Response.json({ error: "Invalid JSON body" }, { status: 400, headers: corsHeaders });
132
+ }
133
+ if (typeof body !== 'object' || body === null) {
134
+ return Response.json({ error: "Request body must be a JSON object" }, { status: 400, headers: corsHeaders });
135
+ }
136
+ const { name } = body as { name?: unknown };
137
+ if (name !== undefined && typeof name !== 'string') {
138
+ return Response.json({ error: "`name` must be a string if provided" }, { status: 400, headers: corsHeaders });
139
+ }
128
140
  const config = loadAuthConfig();
129
141
  const newKey = generateApiKey(name || 'Untitled');
130
142
  config.keys.push(newKey);
@@ -157,6 +169,28 @@ Bun.serve({
157
169
  return Response.json({ ok: true }, { headers: corsHeaders });
158
170
  }
159
171
 
172
+ // ── Dashboard API: model list (bypasses API key auth) ──────────────
173
+ if (url.pathname === "/api/models" && req.method === "GET") {
174
+ try {
175
+ const modelsUrl = new URL('/v1/models', TARGET_URL);
176
+ const response = await fetch(modelsUrl.toString());
177
+ const data = await response.json();
178
+ if (data.data && Array.isArray(data.data)) {
179
+ data.data = data.data.map((model: any) => ({
180
+ ...model,
181
+ id: PREFIX + model.id,
182
+ display_name: PREFIX + (model.display_name || model.id)
183
+ }));
184
+ }
185
+ return new Response(JSON.stringify(data), {
186
+ status: response.status,
187
+ headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
188
+ });
189
+ } catch (e: any) {
190
+ return Response.json({ error: e?.message || 'Failed to fetch models' }, { status: 502, headers: corsHeaders });
191
+ }
192
+ }
193
+
160
194
  // ── Proxy logic ───────────────────────────────────────────────────────
161
195
  const targetUrl = new URL(url.pathname + url.search, TARGET_URL);
162
196
 
@@ -170,9 +204,8 @@ Bun.serve({
170
204
  });
171
205
  }
172
206
 
173
- try {
174
- if (req.method === "POST" && url.pathname.includes("/chat/completions")) {
175
- // Check API key if required
207
+ // ── Enforce API key auth on all /v1/* routes ──────────────────────────
208
+ if (url.pathname.startsWith("/v1/")) {
176
209
  const authConfig = loadAuthConfig();
177
210
  if (authConfig.requireApiKey) {
178
211
  const authHeader = req.headers.get('authorization');
@@ -184,7 +217,10 @@ Bun.serve({
184
217
  );
185
218
  }
186
219
  }
220
+ }
187
221
 
222
+ try {
223
+ if (req.method === "POST" && url.pathname.includes("/chat/completions")) {
188
224
  const startTime = Date.now();
189
225
  let json = await req.json();
190
226
 
@@ -205,38 +241,37 @@ Bun.serve({
205
241
 
206
242
  logTransformedRequest(json);
207
243
 
208
- const body = JSON.stringify(json);
209
244
  const headers = new Headers(req.headers);
210
245
  headers.set("host", targetUrl.host);
211
- headers.set("content-length", String(new TextEncoder().encode(body).length));
246
+ headers.delete("authorization"); // Don't leak proxy API keys upstream
212
247
 
213
248
  const needsResponsesAPI = targetModel.match(/^gpt-5\.[2-9]|^gpt-5\.\d+-codex|^o[1-9]|^goldeneye/i);
214
249
 
215
- // For models that need max_completion_tokens instead of max_tokens
216
- const needsMaxCompletionTokens = targetModel.match(/^gpt-5\.[2-9]|^gpt-5\.\d+-codex|^goldeneye/i);
217
- if (needsMaxCompletionTokens && json.max_tokens) {
250
+ if (needsResponsesAPI && json.max_tokens) {
218
251
  json.max_completion_tokens = json.max_tokens;
219
252
  delete json.max_tokens;
220
253
  console.log(`🔧 Converted max_tokens → max_completion_tokens`);
221
254
  }
222
255
 
223
- // Try Responses API first for models that may need it; fall back to chat completions
224
256
  if (needsResponsesAPI) {
225
- console.log(`🔀 Model ${targetModel} — trying Responses API bridge`);
257
+ console.log(`🔀 Model ${targetModel} — using Responses API bridge`);
226
258
  const chatId = `chatcmpl-proxy-${++responseCounter}`;
227
259
  try {
228
- const result = await handleResponsesAPIBridge(json, req, chatId, TARGET_URL);
229
- if (result.status !== 404) {
230
- addRequestLog({
231
- id: getNextRequestId(), timestamp: startTime, model: targetModel,
232
- promptTokens: 0, completionTokens: 0, totalTokens: 0,
233
- status: result.status, duration: Date.now() - startTime, stream: !!json.stream,
234
- });
235
- return result;
236
- }
237
- console.log(`⚠️ Responses API returned 404 — falling back to chat/completions`);
238
- } catch (e) {
239
- console.log(`⚠️ Responses API failed — falling back to chat/completions`);
260
+ const bridgeResult = await handleResponsesAPIBridge(json, req, chatId, TARGET_URL);
261
+ addRequestLog({
262
+ id: getNextRequestId(), timestamp: startTime, model: targetModel,
263
+ promptTokens: bridgeResult.usage.promptTokens,
264
+ completionTokens: bridgeResult.usage.completionTokens,
265
+ totalTokens: bridgeResult.usage.totalTokens,
266
+ status: bridgeResult.response.status, duration: Date.now() - startTime, stream: !!json.stream,
267
+ });
268
+ return bridgeResult.response;
269
+ } catch (e: any) {
270
+ console.error(`❌ Responses API bridge failed for ${targetModel}:`, e?.message || e);
271
+ return new Response(
272
+ JSON.stringify({ error: { message: `Responses API bridge failed: ${e?.message || 'Unknown error'}`, type: "proxy_error" } }),
273
+ { status: 502, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }
274
+ );
240
275
  }
241
276
  }
242
277
 
@@ -248,6 +283,9 @@ Bun.serve({
248
283
  headers.set("Copilot-Vision-Request", "true");
249
284
  }
250
285
 
286
+ const body = JSON.stringify(json);
287
+ headers.set("content-length", String(new TextEncoder().encode(body).length));
288
+
251
289
  const response = await fetch(targetUrl.toString(), {
252
290
  method: "POST",
253
291
  headers: headers,
@@ -304,21 +342,9 @@ Bun.serve({
304
342
  }
305
343
 
306
344
  if (req.method === "GET" && url.pathname.includes("/models")) {
307
- // Check API key if required
308
- const authConfig = loadAuthConfig();
309
- if (authConfig.requireApiKey) {
310
- const authHeader = req.headers.get('authorization');
311
- const providedKey = authHeader?.replace('Bearer ', '');
312
- if (!providedKey || !validateApiKey(providedKey)) {
313
- return Response.json(
314
- { error: { message: "Invalid API key. Generate one from the dashboard.", type: "invalid_api_key" } },
315
- { status: 401, headers: { "Access-Control-Allow-Origin": "*" } }
316
- );
317
- }
318
- }
319
-
320
345
  const headers = new Headers(req.headers);
321
346
  headers.set("host", targetUrl.host);
347
+ headers.delete("authorization"); // Don't leak proxy API keys upstream
322
348
  const response = await fetch(targetUrl.toString(), { method: "GET", headers: headers });
323
349
  const data = await response.json();
324
350
 
@@ -337,6 +363,7 @@ Bun.serve({
337
363
 
338
364
  const headers = new Headers(req.headers);
339
365
  headers.set("host", targetUrl.host);
366
+ headers.delete("authorization"); // Don't leak proxy API keys upstream
340
367
  const response = await fetch(targetUrl.toString(), {
341
368
  method: req.method,
342
369
  headers: headers,
@@ -1,6 +1,11 @@
1
1
  import { convertResponsesSyncToChatCompletions, convertResponsesStreamToChatCompletions } from './responses-converters';
2
2
 
3
- export async function handleResponsesAPIBridge(json: any, req: Request, chatId: string, targetUrl: string) {
3
+ export interface BridgeResult {
4
+ response: Response;
5
+ usage: { promptTokens: number; completionTokens: number; totalTokens: number };
6
+ }
7
+
8
+ export async function handleResponsesAPIBridge(json: any, req: Request, chatId: string, targetUrl: string): Promise<BridgeResult> {
4
9
  const corsHeaders = { "Access-Control-Allow-Origin": "*" };
5
10
 
6
11
  const responsesReq: any = {
@@ -55,7 +60,7 @@ export async function handleResponsesAPIBridge(json: any, req: Request, chatId:
55
60
 
56
61
  return {
57
62
  role: m.role === 'assistant' ? 'assistant' : 'user',
58
- content: typeof content === 'string' ? content : content,
63
+ content,
59
64
  };
60
65
  }).flat();
61
66
  } else {
@@ -96,6 +101,7 @@ export async function handleResponsesAPIBridge(json: any, req: Request, chatId:
96
101
  headers.set("host", responsesUrl.host);
97
102
  headers.set("content-type", "application/json");
98
103
  headers.set("content-length", String(new TextEncoder().encode(responsesBody).length));
104
+ headers.delete("authorization"); // Don't leak proxy API keys upstream
99
105
 
100
106
  const response = await fetch(responsesUrl.toString(), {
101
107
  method: "POST",
@@ -108,12 +114,28 @@ export async function handleResponsesAPIBridge(json: any, req: Request, chatId:
108
114
  if (!response.ok) {
109
115
  const errText = await response.text();
110
116
  console.error(`❌ Responses API Error (${response.status}):`, errText);
111
- return new Response(errText, { status: response.status, headers: corsHeaders });
117
+ return {
118
+ response: new Response(errText, { status: response.status, headers: corsHeaders }),
119
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
120
+ };
112
121
  }
113
122
 
114
123
  if (json.stream && response.body) {
115
- return convertResponsesStreamToChatCompletions(response, json.model, chatId, corsHeaders);
124
+ return {
125
+ response: convertResponsesStreamToChatCompletions(response, json.model, chatId, corsHeaders),
126
+ // Streaming usage is embedded in the stream; not available synchronously
127
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
128
+ };
116
129
  } else {
117
- return convertResponsesSyncToChatCompletions(response, json.model, chatId, corsHeaders);
130
+ const data = await response.json() as any;
131
+ const usage = data.usage ? {
132
+ promptTokens: data.usage.input_tokens || 0,
133
+ completionTokens: data.usage.output_tokens || 0,
134
+ totalTokens: data.usage.total_tokens || 0,
135
+ } : { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
136
+ return {
137
+ response: convertResponsesSyncToChatCompletions(data, json.model, chatId, corsHeaders),
138
+ usage,
139
+ };
118
140
  }
119
141
  }
@@ -1,5 +1,4 @@
1
- export async function convertResponsesSyncToChatCompletions(response: Response, model: string, chatId: string, corsHeaders: any) {
2
- const data = await response.json() as any;
1
+ export function convertResponsesSyncToChatCompletions(data: any, model: string, chatId: string, corsHeaders: any) {
3
2
  const result: any = {
4
3
  id: chatId,
5
4
  object: 'chat.completion',
@@ -49,7 +48,13 @@ export async function convertResponsesSyncToChatCompletions(response: Response,
49
48
  }
50
49
 
51
50
  export function convertResponsesStreamToChatCompletions(response: Response, model: string, chatId: string, corsHeaders: any) {
52
- const reader = response.body!.getReader();
51
+ if (!response.body) {
52
+ return new Response(JSON.stringify({ error: 'No response body' }), {
53
+ status: 502,
54
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
55
+ });
56
+ }
57
+ const reader = response.body.getReader();
53
58
  const decoder = new TextDecoder();
54
59
  const encoder = new TextEncoder();
55
60
  let buffer = '';
@@ -101,6 +106,7 @@ export function convertResponsesStreamToChatCompletions(response: Response, mode
101
106
  }
102
107
 
103
108
  else if (eventType === 'response.function_call_arguments.delta') {
109
+ if (toolCallIndex < 1) continue; // Guard against out-of-order events
104
110
  controller.enqueue(encoder.encode(makeChatChunk({
105
111
  tool_calls: [{
106
112
  index: toolCallIndex - 1,
package/start.ts CHANGED
@@ -54,7 +54,7 @@ async function main() {
54
54
  const isWindows = process.platform === 'win32';
55
55
  const npxCmd = isWindows ? 'npx.cmd' : 'npx';
56
56
 
57
- copilotProc = spawn([npxCmd, 'copilot-api', 'start'], {
57
+ copilotProc = spawn([npxCmd, '@jeffreycao/copilot-api@latest', 'start'], {
58
58
  stdout: 'pipe',
59
59
  stderr: 'pipe',
60
60
  });
package/usage-db.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { mkdirSync, existsSync, readFileSync, writeFileSync } from 'fs';
1
+ import { mkdirSync, existsSync, readFileSync } from 'fs';
2
+ import { writeFile } from 'fs/promises';
2
3
  import { homedir } from 'os';
3
4
  import { join } from 'path';
4
5
 
@@ -86,7 +87,7 @@ const loadSync = (): UsageData => {
86
87
  const saveToDisk = async () => {
87
88
  try {
88
89
  data.lastSavedAt = Date.now();
89
- writeFileSync(USAGE_FILE, JSON.stringify(data, null, 2), 'utf-8');
90
+ await writeFile(USAGE_FILE, JSON.stringify(data, null, 2), 'utf-8');
90
91
  } catch (e) {
91
92
  console.error('Failed to save usage data:', e);
92
93
  }
@@ -205,6 +206,6 @@ export const flushToDisk = async () => {
205
206
  await saveToDisk();
206
207
  };
207
208
 
208
- process.on('beforeExit', () => { flushToDisk(); });
209
+ process.on('beforeExit', async () => { await flushToDisk(); });
209
210
  process.on('SIGINT', async () => { await flushToDisk(); process.exit(0); });
210
211
  process.on('SIGTERM', async () => { await flushToDisk(); process.exit(0); });