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/README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# 🚀 Copilot Proxy for Cursor
|
|
2
|
+
|
|
3
|
+
> Forked from [jacksonkasi1/copilot-for-cursor](https://github.com/jacksonkasi1/copilot-for-cursor) with fixes for full Anthropic → OpenAI message conversion.
|
|
4
|
+
|
|
5
|
+
**Unlock the full power of GitHub Copilot in Cursor IDE.**
|
|
6
|
+
|
|
7
|
+
This project provides a local proxy server that acts as a bridge between Cursor and GitHub Copilot. It solves key limitations by:
|
|
8
|
+
1. **Bypassing Cursor's Model Routing:** Using a custom prefix (`cus-`) to force Cursor to use your own API endpoint instead of its internal backend.
|
|
9
|
+
2. **Enabling Agentic Capabilities:** Transforming Cursor's Anthropic-style tool calls into OpenAI-compatible formats that Copilot understands. This enables **File Editing, Terminal Execution, Codebase Search, and MCP Tools**.
|
|
10
|
+
3. **Fixing Schema Errors:** Automatically sanitizing requests to prevent `400 Bad Request` errors caused by format mismatches (e.g., `tool_choice`, `cache_control`, unsupported content types).
|
|
11
|
+
|
|
12
|
+
### Changes in this fork
|
|
13
|
+
|
|
14
|
+
- **Full Anthropic → OpenAI message conversion:** Assistant `tool_use` blocks are converted to OpenAI `tool_calls`; user `tool_result` blocks become `tool` role messages. This fixes `unexpected tool_use_id found in tool_result blocks` errors.
|
|
15
|
+
- **Unsupported content type stripping:** Blocks with types like `thinking`, `tool_use` (in user messages), etc. are filtered out before forwarding, preventing `type has to be either 'image_url' or 'text'` errors.
|
|
16
|
+
- **Windows setup instructions** added below.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 🏗 Architecture
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Cursor → (HTTPS tunnel) → proxy-router (:4142) → copilot-api (:4141) → GitHub Copilot
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
* **Port 4141 (`copilot-api`):** The core service that authenticates with GitHub and provides the OpenAI-compatible API.
|
|
27
|
+
* *Powered by [caozhiyuan/copilot-api](https://github.com/caozhiyuan/copilot-api) (installed via `npx`).*
|
|
28
|
+
* **Port 4142 (`proxy-router`):** The intelligence layer. It intercepts requests, converts Anthropic-format messages to OpenAI format, handles the `cus-` prefix, and serves the dashboard.
|
|
29
|
+
* **HTTPS tunnel (Cloudflare/ngrok):** Cursor requires HTTPS — a tunnel exposes the local proxy to the internet.
|
|
30
|
+
|
|
31
|
+
### Proxy Router Modules
|
|
32
|
+
|
|
33
|
+
The proxy router is split into focused modules:
|
|
34
|
+
|
|
35
|
+
| File | Responsibility |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `proxy-router.ts` | Entrypoint — Bun.serve, routing, CORS, dashboard, model list |
|
|
38
|
+
| `anthropic-transforms.ts` | Anthropic → OpenAI normalization (fields, tools, messages) |
|
|
39
|
+
| `responses-bridge.ts` | Chat Completions → Responses API bridge for GPT-5.x / o-series |
|
|
40
|
+
| `responses-converters.ts` | Responses API → Chat Completions format (sync & streaming) |
|
|
41
|
+
| `stream-proxy.ts` | Streaming passthrough with chunk logging and error detection |
|
|
42
|
+
| `debug-logger.ts` | Request/response debug logging helpers |
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 🛠 Setup Guide
|
|
47
|
+
|
|
48
|
+
### Prerequisites
|
|
49
|
+
* [Node.js](https://nodejs.org/) & npm
|
|
50
|
+
* [Bun](https://bun.sh/) (for the proxy-router)
|
|
51
|
+
* A tunnel tool — **Cloudflare Tunnel** (free, no signup) or **ngrok**
|
|
52
|
+
* GitHub account with a **Copilot subscription** (individual, business, or enterprise)
|
|
53
|
+
|
|
54
|
+
### Quick Start (Windows)
|
|
55
|
+
|
|
56
|
+
Open **3 separate terminals** and run each command:
|
|
57
|
+
|
|
58
|
+
**Terminal 1 — Start copilot-api (port 4141):**
|
|
59
|
+
```sh
|
|
60
|
+
npx @jeffreycao/copilot-api@latest start
|
|
61
|
+
```
|
|
62
|
+
> On first run, it will prompt you to authenticate via GitHub device flow.
|
|
63
|
+
|
|
64
|
+
**Terminal 2 — Start proxy-router (port 4142):**
|
|
65
|
+
```sh
|
|
66
|
+
cd copilot-for-cursor
|
|
67
|
+
bun run proxy-router.ts
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Terminal 3 — Start HTTPS tunnel:**
|
|
71
|
+
|
|
72
|
+
Using Cloudflare Tunnel (recommended, free, no signup):
|
|
73
|
+
```sh
|
|
74
|
+
# Install (one-time)
|
|
75
|
+
winget install cloudflare.cloudflared
|
|
76
|
+
|
|
77
|
+
# Run tunnel
|
|
78
|
+
cloudflared tunnel --url http://localhost:4142
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Or using ngrok:
|
|
82
|
+
```sh
|
|
83
|
+
ngrok http 4142
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Copy the HTTPS URL from the tunnel output (e.g., `https://xxxxx.trycloudflare.com`).
|
|
87
|
+
|
|
88
|
+
### Quick Start (macOS)
|
|
89
|
+
|
|
90
|
+
Run the setup scripts for persistent background services:
|
|
91
|
+
```bash
|
|
92
|
+
# 1. Setup Core API (Port 4141)
|
|
93
|
+
chmod +x setup-copilot-service.sh
|
|
94
|
+
./setup-copilot-service.sh
|
|
95
|
+
|
|
96
|
+
# 2. Setup Proxy Router (Port 4142)
|
|
97
|
+
chmod +x setup-proxy-service.sh
|
|
98
|
+
./setup-proxy-service.sh
|
|
99
|
+
|
|
100
|
+
# 3. Start tunnel
|
|
101
|
+
ngrok http 4142
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Verify Services
|
|
105
|
+
Check if the dashboard is running:
|
|
106
|
+
👉 **[http://localhost:4142](http://localhost:4142)**
|
|
107
|
+
|
|
108
|
+

|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## ⚙️ Cursor Configuration
|
|
113
|
+
|
|
114
|
+
1. Go to **Settings** (Gear Icon) → **Models**.
|
|
115
|
+
2. Toggle **OFF** "Copilot" (optional, to avoid conflicts).
|
|
116
|
+
3. Add a new **OpenAI Compatible** model:
|
|
117
|
+
* **Base URL:** `https://your-tunnel-url.trycloudflare.com/v1`
|
|
118
|
+
* **API Key:** `dummy` (any value works, unless you configured `auth.apiKeys` in copilot-api's `config.json`)
|
|
119
|
+
* **Model Name:** Use a **prefixed name** — e.g., `cus-gpt-4o`, `cus-claude-sonnet-4`, `cus-claude-sonnet-4.5`
|
|
120
|
+
|
|
121
|
+
> **💡 Tip:** Go to the [Dashboard](http://localhost:4142) to see all available models and copy their IDs.
|
|
122
|
+
|
|
123
|
+
> **⚠️ Important:** You **must** use the `cus-` prefix. Without it, Cursor routes the request to its own backend instead of your proxy.
|
|
124
|
+
|
|
125
|
+
### Available Models (examples)
|
|
126
|
+
|
|
127
|
+
| Cursor Model Name | Actual Model |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `cus-gpt-4o` | GPT-4o |
|
|
130
|
+
| `cus-gpt-5.4` | GPT-5.4 |
|
|
131
|
+
| `cus-claude-sonnet-4` | Claude Sonnet 4 |
|
|
132
|
+
| `cus-claude-sonnet-4.5` | Claude Sonnet 4.5 |
|
|
133
|
+
| `cus-claude-opus-4.6` | Claude Opus 4.6 |
|
|
134
|
+
| `cus-gemini-2.5-pro` | Gemini 2.5 Pro |
|
|
135
|
+
|
|
136
|
+

|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 🔒 Security: API Key Protection
|
|
141
|
+
|
|
142
|
+
If you're using a tunnel (exposing to the public internet), set an API key in copilot-api's config:
|
|
143
|
+
|
|
144
|
+
**Config location:**
|
|
145
|
+
- Linux/macOS: `~/.local/share/copilot-api/config.json`
|
|
146
|
+
- Windows: `%USERPROFILE%\.local\share\copilot-api\config.json`
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"auth": {
|
|
151
|
+
"apiKeys": ["your-secret-key-here"]
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Then use the same key as the **API Key** in Cursor settings.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## ✨ Features & Supported Tools
|
|
161
|
+
|
|
162
|
+
This proxy enables **full agentic workflows**:
|
|
163
|
+
|
|
164
|
+
* **💬 Chat & Reasoning:** Full conversation context with standard models.
|
|
165
|
+
* **📂 File System:** `Read`, `Write`, `StrReplace`, `Delete`.
|
|
166
|
+
* **💻 Terminal:** `Shell` (Run commands).
|
|
167
|
+
* **🔍 Search:** `Grep`, `Glob`, `SemanticSearch`.
|
|
168
|
+
* **🔌 MCP Tools:** Full support for external tools like Neon, Playwright, etc.
|
|
169
|
+
|
|
170
|
+
### What the proxy handles
|
|
171
|
+
|
|
172
|
+
| Cursor sends (Anthropic format) | Proxy converts to (OpenAI format) |
|
|
173
|
+
|---|---|
|
|
174
|
+
| `tool_use` blocks in assistant messages | `tool_calls` array |
|
|
175
|
+
| `tool_result` blocks in user messages | `tool` role messages |
|
|
176
|
+
| `thinking` blocks | Stripped (not supported) |
|
|
177
|
+
| `cache_control` on content blocks | Stripped |
|
|
178
|
+
| `input_schema` on tools | Converted to `parameters` |
|
|
179
|
+
| Anthropic `tool_choice` objects | OpenAI string format |
|
|
180
|
+
| Images in Claude requests | Stripped with `[Image Omitted]` placeholder |
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## ⚠️ Known Limitations
|
|
185
|
+
|
|
186
|
+
### Claude Vision
|
|
187
|
+
* **Gemini / GPT-4o:** Full Vision Support ✅
|
|
188
|
+
* **Claude (via Copilot):** Does **NOT** support images via the API proxy ❌
|
|
189
|
+
|
|
190
|
+
The proxy automatically strips images from Claude requests to prevent crashes.
|
|
191
|
+
|
|
192
|
+
### What's lost vs native Anthropic API
|
|
193
|
+
Since Cursor sends to `/v1/chat/completions` (OpenAI) instead of `/v1/messages` (Anthropic), some features are unavailable:
|
|
194
|
+
|
|
195
|
+
| Feature | Status |
|
|
196
|
+
|---|---|
|
|
197
|
+
| Extended thinking (chain-of-thought) | ❌ Stripped |
|
|
198
|
+
| Prompt caching (`cache_control`) | ❌ Stripped |
|
|
199
|
+
| Context management beta | ❌ Not available |
|
|
200
|
+
| Premium request optimization | ❌ Bypassed |
|
|
201
|
+
| Basic chat & tool calling | ✅ Works |
|
|
202
|
+
| Streaming | ✅ Works |
|
|
203
|
+
|
|
204
|
+
### Tunnel URL changes on restart
|
|
205
|
+
Cloudflare quick tunnels generate a new URL each time. You'll need to update Cursor settings when you restart the tunnel. Consider a paid plan for a fixed subdomain.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
### 📝 Troubleshooting
|
|
210
|
+
|
|
211
|
+
**"Model name is not valid" in Cursor:**
|
|
212
|
+
Make sure you're using the `cus-` prefix (e.g., `cus-gpt-4o`, not `gpt-4o`).
|
|
213
|
+
|
|
214
|
+
**"connection refused" on tunnel:**
|
|
215
|
+
Ensure all 3 services are running (copilot-api on 4141, proxy-router on 4142, tunnel).
|
|
216
|
+
|
|
217
|
+
**500 errors from copilot-api:**
|
|
218
|
+
Restart copilot-api. If the error mentions `messages`, the proxy should now handle it — make sure you're running the latest `proxy-router.ts`.
|
|
219
|
+
|
|
220
|
+
**Logs (macOS):**
|
|
221
|
+
* Proxy: `tail -f ~/Library/Logs/copilot-proxy.log`
|
|
222
|
+
* API: `tail -f ~/Library/Logs/copilot-api.log`
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
> ⚠️ **DISCLAIMER:** This project is **unofficial** and created for **educational purposes only**. It interacts with undocumented internal APIs of GitHub Copilot and Cursor. Use at your own risk. The authors are not affiliated with GitHub, Microsoft, or Anysphere (Cursor). Please use your API credits responsibly and in accordance with the provider's Terms of Service.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const cleanSchema = (schema: any): any => {
|
|
2
|
+
if (!schema || typeof schema !== 'object') return schema;
|
|
3
|
+
if (schema.additionalProperties !== undefined) delete schema.additionalProperties;
|
|
4
|
+
if (schema.$schema !== undefined) delete schema.$schema;
|
|
5
|
+
if (schema.title !== undefined) delete schema.title;
|
|
6
|
+
if (schema.properties) {
|
|
7
|
+
for (const key in schema.properties) cleanSchema(schema.properties[key]);
|
|
8
|
+
}
|
|
9
|
+
if (schema.items) cleanSchema(schema.items);
|
|
10
|
+
return schema;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const sanitizeContentPart = (part: any, isClaude: boolean): any | null => {
|
|
14
|
+
if (part.cache_control) delete part.cache_control;
|
|
15
|
+
|
|
16
|
+
if (isClaude && (part.type === 'image' || (part.source?.type === 'base64'))) {
|
|
17
|
+
return { type: 'text', text: '[Image Omitted]' };
|
|
18
|
+
}
|
|
19
|
+
if (part.type === 'image' && part.source?.type === 'base64') {
|
|
20
|
+
return { type: 'image_url', image_url: { url: `data:${part.source.media_type};base64,${part.source.data}` } };
|
|
21
|
+
}
|
|
22
|
+
if (part.type === 'image') { part.type = 'image_url'; return part; }
|
|
23
|
+
|
|
24
|
+
if (part.type === 'text' || part.type === 'image_url') return part;
|
|
25
|
+
|
|
26
|
+
return null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const transformAnthropicFields = (json: any): void => {
|
|
30
|
+
if (json.system) {
|
|
31
|
+
const systemText = typeof json.system === 'string'
|
|
32
|
+
? json.system
|
|
33
|
+
: Array.isArray(json.system)
|
|
34
|
+
? json.system.map((s: any) => typeof s === 'string' ? s : s.text || '').join('\n')
|
|
35
|
+
: String(json.system);
|
|
36
|
+
if (json.messages && Array.isArray(json.messages)) {
|
|
37
|
+
const hasSystem = json.messages.some((m: any) => m.role === 'system');
|
|
38
|
+
if (!hasSystem) {
|
|
39
|
+
json.messages.unshift({ role: 'system', content: systemText });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
delete json.system;
|
|
43
|
+
console.log('🔧 Converted top-level system field to system message');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (json.stop_sequences) {
|
|
47
|
+
json.stop = json.stop_sequences;
|
|
48
|
+
delete json.stop_sequences;
|
|
49
|
+
console.log('🔧 Converted stop_sequences → stop');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (json.max_tokens_to_sample && !json.max_tokens) {
|
|
53
|
+
json.max_tokens = json.max_tokens_to_sample;
|
|
54
|
+
delete json.max_tokens_to_sample;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const anthropicOnlyFields = ['metadata', 'anthropic_version', 'top_k'];
|
|
58
|
+
for (const field of anthropicOnlyFields) {
|
|
59
|
+
if (json[field] !== undefined) {
|
|
60
|
+
console.log(`🔧 Removing Anthropic-only field: ${field}`);
|
|
61
|
+
delete json[field];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const transformTools = (json: any): void => {
|
|
67
|
+
if (!json.tools || !Array.isArray(json.tools)) return;
|
|
68
|
+
|
|
69
|
+
json.tools = json.tools.map((tool: any) => {
|
|
70
|
+
let parameters = tool.input_schema || tool.parameters || {};
|
|
71
|
+
parameters = cleanSchema(parameters);
|
|
72
|
+
if (tool.type === 'function' && tool.function) {
|
|
73
|
+
tool.function.parameters = cleanSchema(tool.function.parameters);
|
|
74
|
+
return tool;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
type: "function",
|
|
78
|
+
function: {
|
|
79
|
+
name: tool.name,
|
|
80
|
+
description: tool.description,
|
|
81
|
+
parameters: parameters
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const transformToolChoice = (json: any): void => {
|
|
88
|
+
if (!json.tool_choice || typeof json.tool_choice !== 'object') return;
|
|
89
|
+
|
|
90
|
+
if (json.tool_choice.type === 'auto') json.tool_choice = "auto";
|
|
91
|
+
else if (json.tool_choice.type === 'none') json.tool_choice = "none";
|
|
92
|
+
else if (json.tool_choice.type === 'required') json.tool_choice = "required";
|
|
93
|
+
else if (json.tool_choice.type === 'any') json.tool_choice = "required";
|
|
94
|
+
else if (json.tool_choice.type === 'tool' && json.tool_choice.name) {
|
|
95
|
+
json.tool_choice = { type: "function", function: { name: json.tool_choice.name } };
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const transformMessages = (json: any, isClaude: boolean): void => {
|
|
100
|
+
if (!json.messages || !Array.isArray(json.messages)) return;
|
|
101
|
+
|
|
102
|
+
const newMessages: any[] = [];
|
|
103
|
+
|
|
104
|
+
for (const msg of json.messages) {
|
|
105
|
+
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
|
|
106
|
+
const textParts: string[] = [];
|
|
107
|
+
const toolCalls: any[] = [];
|
|
108
|
+
|
|
109
|
+
for (const part of msg.content) {
|
|
110
|
+
if (part.type === 'tool_use') {
|
|
111
|
+
toolCalls.push({
|
|
112
|
+
id: part.id,
|
|
113
|
+
type: 'function',
|
|
114
|
+
function: {
|
|
115
|
+
name: part.name,
|
|
116
|
+
arguments: typeof part.input === 'string' ? part.input : JSON.stringify(part.input ?? {})
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
} else if (part.type === 'text') {
|
|
120
|
+
textParts.push(part.text);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const assistantMsg: any = { role: 'assistant' };
|
|
125
|
+
assistantMsg.content = textParts.join('\n') || null;
|
|
126
|
+
if (toolCalls.length > 0) assistantMsg.tool_calls = toolCalls;
|
|
127
|
+
newMessages.push(assistantMsg);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (msg.role === 'user' && Array.isArray(msg.content)) {
|
|
132
|
+
const toolResults = msg.content.filter((c: any) => c.type === 'tool_result');
|
|
133
|
+
const otherParts = msg.content.filter((c: any) => c.type !== 'tool_result' && c.type !== 'tool_use');
|
|
134
|
+
|
|
135
|
+
for (const tr of toolResults) {
|
|
136
|
+
let resultContent = tr.content;
|
|
137
|
+
if (typeof resultContent !== 'string') {
|
|
138
|
+
if (Array.isArray(resultContent)) {
|
|
139
|
+
resultContent = resultContent.map((p: any) => p.text || JSON.stringify(p)).join('\n');
|
|
140
|
+
} else {
|
|
141
|
+
resultContent = JSON.stringify(resultContent);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
newMessages.push({
|
|
145
|
+
role: 'tool',
|
|
146
|
+
tool_call_id: tr.tool_use_id,
|
|
147
|
+
content: resultContent || ''
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (otherParts.length > 0) {
|
|
152
|
+
const cleaned = otherParts.map((p: any) => sanitizeContentPart(p, isClaude)).filter(Boolean);
|
|
153
|
+
if (cleaned.length > 0) {
|
|
154
|
+
newMessages.push({ role: 'user', content: cleaned });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (Array.isArray(msg.content)) {
|
|
161
|
+
const cleaned = msg.content.map((p: any) => sanitizeContentPart(p, isClaude)).filter(Boolean);
|
|
162
|
+
msg.content = cleaned.length > 0 ? cleaned : ' ';
|
|
163
|
+
}
|
|
164
|
+
newMessages.push(msg);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
json.messages = newMessages;
|
|
168
|
+
|
|
169
|
+
for (let i = 0; i < json.messages.length; i++) {
|
|
170
|
+
const msg = json.messages[i];
|
|
171
|
+
if (Array.isArray(msg.content) && msg.content.length === 0) {
|
|
172
|
+
msg.content = ' ';
|
|
173
|
+
}
|
|
174
|
+
if (Array.isArray(msg.content) && msg.content.length === 1 && msg.content[0].type === 'text') {
|
|
175
|
+
msg.content = msg.content[0].text || ' ';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const normalizeRequest = (json: any, isClaude: boolean): void => {
|
|
181
|
+
transformAnthropicFields(json);
|
|
182
|
+
transformTools(json);
|
|
183
|
+
transformToolChoice(json);
|
|
184
|
+
transformMessages(json, isClaude);
|
|
185
|
+
};
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { execSync, spawn } = require('child_process');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
// Check if bun is installed
|
|
7
|
+
let bunPath;
|
|
8
|
+
try {
|
|
9
|
+
bunPath = execSync('where bun', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0];
|
|
10
|
+
} catch {
|
|
11
|
+
try {
|
|
12
|
+
bunPath = execSync('which bun', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
13
|
+
} catch {
|
|
14
|
+
console.error('❌ Bun is required but not installed.');
|
|
15
|
+
console.error(' Install it with: curl -fsSL https://bun.sh/install | bash');
|
|
16
|
+
console.error(' Or: npm install -g bun');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Find start.ts relative to this script
|
|
22
|
+
const pkgRoot = path.resolve(__dirname, '..');
|
|
23
|
+
const startScript = path.join(pkgRoot, 'start.ts');
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(startScript)) {
|
|
26
|
+
console.error('❌ start.ts not found at:', startScript);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Forward all args and run with bun
|
|
31
|
+
const args = ['run', startScript, ...process.argv.slice(2)];
|
|
32
|
+
const child = spawn(bunPath.trim(), args, {
|
|
33
|
+
stdio: 'inherit',
|
|
34
|
+
cwd: pkgRoot,
|
|
35
|
+
env: { ...process.env },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
child.on('error', (err) => {
|
|
39
|
+
console.error('❌ Failed to start:', err.message);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
child.on('exit', (code) => {
|
|
44
|
+
process.exit(code || 0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Forward signals for graceful shutdown
|
|
48
|
+
process.on('SIGINT', () => child.kill('SIGINT'));
|
|
49
|
+
process.on('SIGTERM', () => child.kill('SIGTERM'));
|