claude-code-relay 0.0.4 → 0.0.8
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 +199 -0
- package/dist/cjs/cli-wrapper.js +34 -7
- package/dist/cjs/cli.js +55 -9
- package/dist/cjs/index.js +52 -8
- package/dist/cjs/server.js +52 -8
- package/dist/esm/cli-wrapper.d.ts.map +1 -1
- package/dist/esm/cli-wrapper.js +34 -7
- package/dist/esm/cli.js +55 -9
- package/dist/esm/index.js +52 -8
- package/dist/esm/server.d.ts +1 -0
- package/dist/esm/server.d.ts.map +1 -1
- package/dist/esm/server.js +52 -8
- package/package.json +4 -1
package/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# claude-code-relay
|
|
2
|
+
|
|
3
|
+
Local proxy that exposes Claude CLI as an OpenAI-compatible API server.
|
|
4
|
+
|
|
5
|
+
Use your existing Claude CLI installation with any OpenAI-compatible client.
|
|
6
|
+
|
|
7
|
+
## Why?
|
|
8
|
+
|
|
9
|
+
- You already have Claude CLI working with your subscription
|
|
10
|
+
- You want to use tools that expect OpenAI API format
|
|
11
|
+
- No separate API key needed - uses your local Claude CLI
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### Node.js / Bun
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx claude-code-relay serve
|
|
19
|
+
# or
|
|
20
|
+
bunx claude-code-relay serve
|
|
21
|
+
# or install globally
|
|
22
|
+
npm install -g claude-code-relay
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Python
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install claude-code-relay
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Start the server
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Node
|
|
37
|
+
npx claude-code-relay serve --port 52014
|
|
38
|
+
|
|
39
|
+
# Python
|
|
40
|
+
claude-code-relay serve --port 52014
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Use with OpenAI SDK
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from openai import OpenAI
|
|
47
|
+
|
|
48
|
+
client = OpenAI(
|
|
49
|
+
base_url="http://localhost:52014/v1",
|
|
50
|
+
api_key="not-needed"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
response = client.chat.completions.create(
|
|
54
|
+
model="sonnet", # or "opus", "haiku"
|
|
55
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
56
|
+
)
|
|
57
|
+
print(response.choices[0].message.content)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import OpenAI from "openai";
|
|
62
|
+
|
|
63
|
+
const client = new OpenAI({
|
|
64
|
+
baseURL: "http://localhost:52014/v1",
|
|
65
|
+
apiKey: "not-needed",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const response = await client.chat.completions.create({
|
|
69
|
+
model: "sonnet",
|
|
70
|
+
messages: [{ role: "user", content: "Hello!" }],
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Use with LiteLLM
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from litellm import completion
|
|
78
|
+
|
|
79
|
+
response = completion(
|
|
80
|
+
model="openai/sonnet",
|
|
81
|
+
api_base="http://localhost:52014/v1",
|
|
82
|
+
api_key="not-needed",
|
|
83
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Use with Vercel AI SDK
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
91
|
+
import { generateText } from "ai";
|
|
92
|
+
|
|
93
|
+
const claude = createOpenAICompatible({
|
|
94
|
+
name: "claude-code-relay",
|
|
95
|
+
baseURL: "http://localhost:52014/v1",
|
|
96
|
+
apiKey: "not-needed",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const { text } = await generateText({
|
|
100
|
+
model: claude.chatModel("sonnet"),
|
|
101
|
+
prompt: "Hello!",
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## API Endpoints
|
|
106
|
+
|
|
107
|
+
| Endpoint | Method | Description |
|
|
108
|
+
|----------|--------|-------------|
|
|
109
|
+
| `/v1/chat/completions` | POST | Chat completions (streaming supported) |
|
|
110
|
+
| `/v1/models` | GET | List available models |
|
|
111
|
+
| `/health` | GET | Health check |
|
|
112
|
+
|
|
113
|
+
## OpenAI API Compatibility
|
|
114
|
+
|
|
115
|
+
### Supported Features
|
|
116
|
+
|
|
117
|
+
| Feature | Status | Notes |
|
|
118
|
+
|---------|--------|-------|
|
|
119
|
+
| `model` | Supported | `sonnet`, `opus`, `haiku` (+ aliases below) |
|
|
120
|
+
| `messages` | Supported | `system`, `user`, `assistant` roles |
|
|
121
|
+
| `stream` | Supported | SSE streaming |
|
|
122
|
+
| System prompts | Supported | Via `system` role in messages |
|
|
123
|
+
|
|
124
|
+
### Model Aliases
|
|
125
|
+
|
|
126
|
+
These model names are normalized to Claude CLI format:
|
|
127
|
+
|
|
128
|
+
| Input | Maps to |
|
|
129
|
+
|-------|---------|
|
|
130
|
+
| `sonnet` | `sonnet` |
|
|
131
|
+
| `opus` | `opus` |
|
|
132
|
+
| `haiku` | `haiku` |
|
|
133
|
+
| `claude-3-sonnet` | `sonnet` |
|
|
134
|
+
| `claude-3-opus` | `opus` |
|
|
135
|
+
| `claude-3-haiku` | `haiku` |
|
|
136
|
+
| `claude-sonnet-4` | `sonnet` |
|
|
137
|
+
| `claude-opus-4` | `opus` |
|
|
138
|
+
|
|
139
|
+
### Not Supported
|
|
140
|
+
|
|
141
|
+
These parameters are accepted but **ignored** (not passed to Claude CLI):
|
|
142
|
+
|
|
143
|
+
| Parameter | Status |
|
|
144
|
+
|-----------|--------|
|
|
145
|
+
| `temperature` | Ignored |
|
|
146
|
+
| `max_tokens` | Ignored |
|
|
147
|
+
| `top_p` | Ignored |
|
|
148
|
+
| `stop` | Ignored |
|
|
149
|
+
| `n` | Not supported |
|
|
150
|
+
| `presence_penalty` | Not supported |
|
|
151
|
+
| `frequency_penalty` | Not supported |
|
|
152
|
+
| `logit_bias` | Not supported |
|
|
153
|
+
| `response_format` | Not supported |
|
|
154
|
+
| `tools` / `functions` | Not supported |
|
|
155
|
+
| `tool_choice` | Not supported |
|
|
156
|
+
| `seed` | Not supported |
|
|
157
|
+
| `logprobs` | Not supported |
|
|
158
|
+
| `user` | Not supported |
|
|
159
|
+
|
|
160
|
+
### Response Limitations
|
|
161
|
+
|
|
162
|
+
- `usage` tokens are always `0` (not tracked by Claude CLI)
|
|
163
|
+
- `finish_reason` is always `"stop"` (no length/tool_calls detection)
|
|
164
|
+
|
|
165
|
+
## Configuration
|
|
166
|
+
|
|
167
|
+
### Environment Variables
|
|
168
|
+
|
|
169
|
+
| Variable | Default | Description |
|
|
170
|
+
|----------|---------|-------------|
|
|
171
|
+
| `CLAUDE_CODE_RELAY_PORT` | `52014` | Server port (Python only) |
|
|
172
|
+
| `CLAUDE_CODE_RELAY_HOST` | `127.0.0.1` | Host to bind (Python only) |
|
|
173
|
+
| `CLAUDE_CLI_PATH` | `claude` | Path to Claude CLI binary |
|
|
174
|
+
| `CLAUDE_CODE_RELAY_TIMEOUT` | `300` | Request timeout in seconds |
|
|
175
|
+
| `CLAUDE_CODE_RELAY_VERBOSE` | `false` | Enable verbose logging (`1` or `true`) |
|
|
176
|
+
|
|
177
|
+
### CLI Options
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
claude-code-relay serve [options]
|
|
181
|
+
|
|
182
|
+
Options:
|
|
183
|
+
--port, -p <port> Server port (default: 52014)
|
|
184
|
+
--host <host> Host to bind (default: 127.0.0.1)
|
|
185
|
+
--claude-path <path> Path to Claude CLI
|
|
186
|
+
--timeout <seconds> Request timeout (default: 300)
|
|
187
|
+
--verbose, -v Enable verbose logging
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Requirements
|
|
191
|
+
|
|
192
|
+
- Claude CLI installed and authenticated
|
|
193
|
+
- Python 3.10+ or Node.js 18+
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT - see [LICENSE](LICENSE)
|
|
198
|
+
|
|
199
|
+
**Disclaimer**: Unofficial community project. Users are responsible for compliance with Anthropic's Terms of Service.
|
package/dist/cjs/cli-wrapper.js
CHANGED
|
@@ -110,8 +110,10 @@ var ClaudeCLI = class {
|
|
|
110
110
|
const normalizedModel = this.normalizeModel(model);
|
|
111
111
|
return new Promise((resolve, reject) => {
|
|
112
112
|
const args = ["-p", "--model", normalizedModel, "--output-format", "text"];
|
|
113
|
+
const startTime = Date.now();
|
|
113
114
|
if (this.config.verbose) {
|
|
114
|
-
console.log(`
|
|
115
|
+
console.log(`[claude] spawning: ${this.config.cliPath} ${args.join(" ")}`);
|
|
116
|
+
console.log(`[claude] prompt length: ${prompt.length} chars`);
|
|
115
117
|
}
|
|
116
118
|
const proc = (0, import_node_child_process2.spawn)(this.config.cliPath, args, {
|
|
117
119
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -125,6 +127,10 @@ var ClaudeCLI = class {
|
|
|
125
127
|
stderr += data.toString();
|
|
126
128
|
});
|
|
127
129
|
proc.on("close", (code) => {
|
|
130
|
+
const elapsed = Date.now() - startTime;
|
|
131
|
+
if (this.config.verbose) {
|
|
132
|
+
console.log(`[claude] process exited code=${code} elapsed=${elapsed}ms output=${stdout.length} chars`);
|
|
133
|
+
}
|
|
128
134
|
if (code !== 0) {
|
|
129
135
|
reject(new Error(`Claude CLI failed: ${stderr}`));
|
|
130
136
|
} else {
|
|
@@ -145,18 +151,30 @@ var ClaudeCLI = class {
|
|
|
145
151
|
async *stream(messages, model = "sonnet", systemPrompt) {
|
|
146
152
|
const prompt = this.buildPrompt(messages, systemPrompt);
|
|
147
153
|
const normalizedModel = this.normalizeModel(model);
|
|
148
|
-
const
|
|
154
|
+
const startTime = Date.now();
|
|
155
|
+
const args = ["-p", "--verbose", "--model", normalizedModel, "--output-format", "stream-json"];
|
|
149
156
|
if (this.config.verbose) {
|
|
150
|
-
console.log(`
|
|
157
|
+
console.log(`[claude] spawning stream: ${this.config.cliPath} ${args.join(" ")}`);
|
|
158
|
+
console.log(`[claude] prompt length: ${prompt.length} chars`);
|
|
151
159
|
}
|
|
152
160
|
const proc = (0, import_node_child_process2.spawn)(this.config.cliPath, args, {
|
|
153
161
|
stdio: ["pipe", "pipe", "pipe"]
|
|
154
162
|
});
|
|
163
|
+
proc.on("close", (code) => {
|
|
164
|
+
if (this.config.verbose) {
|
|
165
|
+
const elapsed = Date.now() - startTime;
|
|
166
|
+
console.log(`[claude] stream process exited code=${code} elapsed=${elapsed}ms`);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
155
169
|
proc.stdin.write(prompt);
|
|
156
170
|
proc.stdin.end();
|
|
157
171
|
let buffer = "";
|
|
158
172
|
for await (const chunk of proc.stdout) {
|
|
159
|
-
|
|
173
|
+
const chunkStr = chunk.toString();
|
|
174
|
+
buffer += chunkStr;
|
|
175
|
+
if (this.config.verbose) {
|
|
176
|
+
console.log(`[claude] stdout chunk: ${chunkStr.length} bytes`);
|
|
177
|
+
}
|
|
160
178
|
while (buffer.includes("\n")) {
|
|
161
179
|
const [line, rest] = buffer.split("\n", 2);
|
|
162
180
|
buffer = rest ?? "";
|
|
@@ -164,12 +182,21 @@ var ClaudeCLI = class {
|
|
|
164
182
|
if (!trimmed) continue;
|
|
165
183
|
try {
|
|
166
184
|
const data = JSON.parse(trimmed);
|
|
167
|
-
if (
|
|
185
|
+
if (this.config.verbose) {
|
|
186
|
+
console.log(`[claude] parsed type=${data.type}`);
|
|
187
|
+
}
|
|
188
|
+
if (data.type === "assistant" && data.message?.content) {
|
|
189
|
+
for (const block of data.message.content) {
|
|
190
|
+
if (block.type === "text" && block.text) {
|
|
191
|
+
yield block.text;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} else if (data.type === "content_block_delta" && data.delta?.text) {
|
|
195
|
+
yield data.delta.text;
|
|
196
|
+
} else if (data.content) {
|
|
168
197
|
yield data.content;
|
|
169
198
|
} else if (data.text) {
|
|
170
199
|
yield data.text;
|
|
171
|
-
} else if (data.delta?.text) {
|
|
172
|
-
yield data.delta.text;
|
|
173
200
|
}
|
|
174
201
|
} catch {
|
|
175
202
|
if (!trimmed.startsWith("{")) {
|
package/dist/cjs/cli.js
CHANGED
|
@@ -99,8 +99,10 @@ var ClaudeCLI = class {
|
|
|
99
99
|
const normalizedModel = this.normalizeModel(model);
|
|
100
100
|
return new Promise((resolve, reject) => {
|
|
101
101
|
const args = ["-p", "--model", normalizedModel, "--output-format", "text"];
|
|
102
|
+
const startTime = Date.now();
|
|
102
103
|
if (this.config.verbose) {
|
|
103
|
-
console.log(`
|
|
104
|
+
console.log(`[claude] spawning: ${this.config.cliPath} ${args.join(" ")}`);
|
|
105
|
+
console.log(`[claude] prompt length: ${prompt.length} chars`);
|
|
104
106
|
}
|
|
105
107
|
const proc = (0, import_node_child_process2.spawn)(this.config.cliPath, args, {
|
|
106
108
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -114,6 +116,10 @@ var ClaudeCLI = class {
|
|
|
114
116
|
stderr += data.toString();
|
|
115
117
|
});
|
|
116
118
|
proc.on("close", (code) => {
|
|
119
|
+
const elapsed = Date.now() - startTime;
|
|
120
|
+
if (this.config.verbose) {
|
|
121
|
+
console.log(`[claude] process exited code=${code} elapsed=${elapsed}ms output=${stdout.length} chars`);
|
|
122
|
+
}
|
|
117
123
|
if (code !== 0) {
|
|
118
124
|
reject(new Error(`Claude CLI failed: ${stderr}`));
|
|
119
125
|
} else {
|
|
@@ -134,18 +140,30 @@ var ClaudeCLI = class {
|
|
|
134
140
|
async *stream(messages, model = "sonnet", systemPrompt) {
|
|
135
141
|
const prompt = this.buildPrompt(messages, systemPrompt);
|
|
136
142
|
const normalizedModel = this.normalizeModel(model);
|
|
137
|
-
const
|
|
143
|
+
const startTime = Date.now();
|
|
144
|
+
const args = ["-p", "--verbose", "--model", normalizedModel, "--output-format", "stream-json"];
|
|
138
145
|
if (this.config.verbose) {
|
|
139
|
-
console.log(`
|
|
146
|
+
console.log(`[claude] spawning stream: ${this.config.cliPath} ${args.join(" ")}`);
|
|
147
|
+
console.log(`[claude] prompt length: ${prompt.length} chars`);
|
|
140
148
|
}
|
|
141
149
|
const proc = (0, import_node_child_process2.spawn)(this.config.cliPath, args, {
|
|
142
150
|
stdio: ["pipe", "pipe", "pipe"]
|
|
143
151
|
});
|
|
152
|
+
proc.on("close", (code) => {
|
|
153
|
+
if (this.config.verbose) {
|
|
154
|
+
const elapsed = Date.now() - startTime;
|
|
155
|
+
console.log(`[claude] stream process exited code=${code} elapsed=${elapsed}ms`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
144
158
|
proc.stdin.write(prompt);
|
|
145
159
|
proc.stdin.end();
|
|
146
160
|
let buffer = "";
|
|
147
161
|
for await (const chunk of proc.stdout) {
|
|
148
|
-
|
|
162
|
+
const chunkStr = chunk.toString();
|
|
163
|
+
buffer += chunkStr;
|
|
164
|
+
if (this.config.verbose) {
|
|
165
|
+
console.log(`[claude] stdout chunk: ${chunkStr.length} bytes`);
|
|
166
|
+
}
|
|
149
167
|
while (buffer.includes("\n")) {
|
|
150
168
|
const [line, rest] = buffer.split("\n", 2);
|
|
151
169
|
buffer = rest ?? "";
|
|
@@ -153,12 +171,21 @@ var ClaudeCLI = class {
|
|
|
153
171
|
if (!trimmed) continue;
|
|
154
172
|
try {
|
|
155
173
|
const data = JSON.parse(trimmed);
|
|
156
|
-
if (
|
|
174
|
+
if (this.config.verbose) {
|
|
175
|
+
console.log(`[claude] parsed type=${data.type}`);
|
|
176
|
+
}
|
|
177
|
+
if (data.type === "assistant" && data.message?.content) {
|
|
178
|
+
for (const block of data.message.content) {
|
|
179
|
+
if (block.type === "text" && block.text) {
|
|
180
|
+
yield block.text;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} else if (data.type === "content_block_delta" && data.delta?.text) {
|
|
184
|
+
yield data.delta.text;
|
|
185
|
+
} else if (data.content) {
|
|
157
186
|
yield data.content;
|
|
158
187
|
} else if (data.text) {
|
|
159
188
|
yield data.text;
|
|
160
|
-
} else if (data.delta?.text) {
|
|
161
|
-
yield data.delta.text;
|
|
162
189
|
}
|
|
163
190
|
} catch {
|
|
164
191
|
if (!trimmed.startsWith("{")) {
|
|
@@ -171,6 +198,13 @@ var ClaudeCLI = class {
|
|
|
171
198
|
};
|
|
172
199
|
|
|
173
200
|
// src/server.ts
|
|
201
|
+
var _verbose = false;
|
|
202
|
+
function log(...args) {
|
|
203
|
+
if (_verbose) {
|
|
204
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
205
|
+
console.log(`[${timestamp}]`, ...args);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
174
208
|
var _cli = null;
|
|
175
209
|
function sendJson(res, data, status = 200) {
|
|
176
210
|
const body = JSON.stringify(data);
|
|
@@ -241,6 +275,9 @@ async function handleRequest(req, res) {
|
|
|
241
275
|
}
|
|
242
276
|
const chatId = generateId("chatcmpl");
|
|
243
277
|
const created = Math.floor(Date.now() / 1e3);
|
|
278
|
+
const msgCount = request.messages?.length ?? 0;
|
|
279
|
+
const lastMsg = request.messages?.[msgCount - 1]?.content?.slice(0, 30) ?? "";
|
|
280
|
+
log(`\u2192 POST /v1/chat/completions model=${request.model} stream=${!!request.stream} msgs=${msgCount} "${lastMsg}${lastMsg.length >= 30 ? "..." : ""}"`);
|
|
244
281
|
if (request.stream) {
|
|
245
282
|
res.writeHead(200, {
|
|
246
283
|
"Content-Type": "text/event-stream",
|
|
@@ -257,8 +294,10 @@ async function handleRequest(req, res) {
|
|
|
257
294
|
choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }]
|
|
258
295
|
};
|
|
259
296
|
sendSSEChunk(res, JSON.stringify(initial));
|
|
297
|
+
let totalLen = 0;
|
|
260
298
|
try {
|
|
261
299
|
for await (const text of _cli.stream(request.messages, request.model)) {
|
|
300
|
+
totalLen += text.length;
|
|
262
301
|
const chunk = {
|
|
263
302
|
id: chatId,
|
|
264
303
|
object: "chat.completion.chunk",
|
|
@@ -268,7 +307,9 @@ async function handleRequest(req, res) {
|
|
|
268
307
|
};
|
|
269
308
|
sendSSEChunk(res, JSON.stringify(chunk));
|
|
270
309
|
}
|
|
310
|
+
log(`\u2190 stream complete, total length=${totalLen}`);
|
|
271
311
|
} catch (err) {
|
|
312
|
+
log(`\u2190 stream error: ${err}`);
|
|
272
313
|
sendSSEChunk(res, JSON.stringify({ error: { message: String(err), type: "server_error" } }));
|
|
273
314
|
}
|
|
274
315
|
const final = {
|
|
@@ -285,6 +326,7 @@ async function handleRequest(req, res) {
|
|
|
285
326
|
}
|
|
286
327
|
try {
|
|
287
328
|
const content = await _cli.complete(request.messages, request.model);
|
|
329
|
+
log(`\u2190 response complete, length=${content.length}`);
|
|
288
330
|
const response = {
|
|
289
331
|
id: chatId,
|
|
290
332
|
object: "chat.completion",
|
|
@@ -295,6 +337,7 @@ async function handleRequest(req, res) {
|
|
|
295
337
|
};
|
|
296
338
|
sendJson(res, response);
|
|
297
339
|
} catch (err) {
|
|
340
|
+
log(`\u2190 error: ${err}`);
|
|
298
341
|
sendError(res, String(err));
|
|
299
342
|
}
|
|
300
343
|
return;
|
|
@@ -302,8 +345,9 @@ async function handleRequest(req, res) {
|
|
|
302
345
|
sendError(res, "Not found", 404);
|
|
303
346
|
}
|
|
304
347
|
function createApp(config) {
|
|
348
|
+
_verbose = config?.verbose ?? process.env.CLAUDE_CODE_RELAY_VERBOSE === "1";
|
|
305
349
|
try {
|
|
306
|
-
_cli = new ClaudeCLI(config?.cli);
|
|
350
|
+
_cli = new ClaudeCLI({ ...config?.cli, verbose: _verbose });
|
|
307
351
|
} catch (err) {
|
|
308
352
|
console.error(`Failed to initialize Claude CLI: ${err}`);
|
|
309
353
|
_cli = null;
|
|
@@ -376,11 +420,13 @@ if (command === "serve") {
|
|
|
376
420
|
console.log(` Port: ${port}`);
|
|
377
421
|
console.log(` Claude CLI: ${claudePath}`);
|
|
378
422
|
console.log(` Timeout: ${timeout}s`);
|
|
423
|
+
console.log(` Verbose: ${verbose}`);
|
|
379
424
|
console.log();
|
|
380
425
|
console.log(`API endpoint: http://${host}:${port}/v1/chat/completions`);
|
|
381
426
|
console.log();
|
|
382
427
|
runServer(host, port, {
|
|
383
|
-
cli: { cliPath: claudePath, timeout, verbose }
|
|
428
|
+
cli: { cliPath: claudePath, timeout, verbose },
|
|
429
|
+
verbose
|
|
384
430
|
});
|
|
385
431
|
} else if (command === "check") {
|
|
386
432
|
console.log("Checking Claude CLI...");
|
package/dist/cjs/index.js
CHANGED
|
@@ -121,8 +121,10 @@ var ClaudeCLI = class {
|
|
|
121
121
|
const normalizedModel = this.normalizeModel(model);
|
|
122
122
|
return new Promise((resolve, reject) => {
|
|
123
123
|
const args = ["-p", "--model", normalizedModel, "--output-format", "text"];
|
|
124
|
+
const startTime = Date.now();
|
|
124
125
|
if (this.config.verbose) {
|
|
125
|
-
console.log(`
|
|
126
|
+
console.log(`[claude] spawning: ${this.config.cliPath} ${args.join(" ")}`);
|
|
127
|
+
console.log(`[claude] prompt length: ${prompt.length} chars`);
|
|
126
128
|
}
|
|
127
129
|
const proc = (0, import_node_child_process2.spawn)(this.config.cliPath, args, {
|
|
128
130
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -136,6 +138,10 @@ var ClaudeCLI = class {
|
|
|
136
138
|
stderr += data.toString();
|
|
137
139
|
});
|
|
138
140
|
proc.on("close", (code) => {
|
|
141
|
+
const elapsed = Date.now() - startTime;
|
|
142
|
+
if (this.config.verbose) {
|
|
143
|
+
console.log(`[claude] process exited code=${code} elapsed=${elapsed}ms output=${stdout.length} chars`);
|
|
144
|
+
}
|
|
139
145
|
if (code !== 0) {
|
|
140
146
|
reject(new Error(`Claude CLI failed: ${stderr}`));
|
|
141
147
|
} else {
|
|
@@ -156,18 +162,30 @@ var ClaudeCLI = class {
|
|
|
156
162
|
async *stream(messages, model = "sonnet", systemPrompt) {
|
|
157
163
|
const prompt = this.buildPrompt(messages, systemPrompt);
|
|
158
164
|
const normalizedModel = this.normalizeModel(model);
|
|
159
|
-
const
|
|
165
|
+
const startTime = Date.now();
|
|
166
|
+
const args = ["-p", "--verbose", "--model", normalizedModel, "--output-format", "stream-json"];
|
|
160
167
|
if (this.config.verbose) {
|
|
161
|
-
console.log(`
|
|
168
|
+
console.log(`[claude] spawning stream: ${this.config.cliPath} ${args.join(" ")}`);
|
|
169
|
+
console.log(`[claude] prompt length: ${prompt.length} chars`);
|
|
162
170
|
}
|
|
163
171
|
const proc = (0, import_node_child_process2.spawn)(this.config.cliPath, args, {
|
|
164
172
|
stdio: ["pipe", "pipe", "pipe"]
|
|
165
173
|
});
|
|
174
|
+
proc.on("close", (code) => {
|
|
175
|
+
if (this.config.verbose) {
|
|
176
|
+
const elapsed = Date.now() - startTime;
|
|
177
|
+
console.log(`[claude] stream process exited code=${code} elapsed=${elapsed}ms`);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
166
180
|
proc.stdin.write(prompt);
|
|
167
181
|
proc.stdin.end();
|
|
168
182
|
let buffer = "";
|
|
169
183
|
for await (const chunk of proc.stdout) {
|
|
170
|
-
|
|
184
|
+
const chunkStr = chunk.toString();
|
|
185
|
+
buffer += chunkStr;
|
|
186
|
+
if (this.config.verbose) {
|
|
187
|
+
console.log(`[claude] stdout chunk: ${chunkStr.length} bytes`);
|
|
188
|
+
}
|
|
171
189
|
while (buffer.includes("\n")) {
|
|
172
190
|
const [line, rest] = buffer.split("\n", 2);
|
|
173
191
|
buffer = rest ?? "";
|
|
@@ -175,12 +193,21 @@ var ClaudeCLI = class {
|
|
|
175
193
|
if (!trimmed) continue;
|
|
176
194
|
try {
|
|
177
195
|
const data = JSON.parse(trimmed);
|
|
178
|
-
if (
|
|
196
|
+
if (this.config.verbose) {
|
|
197
|
+
console.log(`[claude] parsed type=${data.type}`);
|
|
198
|
+
}
|
|
199
|
+
if (data.type === "assistant" && data.message?.content) {
|
|
200
|
+
for (const block of data.message.content) {
|
|
201
|
+
if (block.type === "text" && block.text) {
|
|
202
|
+
yield block.text;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} else if (data.type === "content_block_delta" && data.delta?.text) {
|
|
206
|
+
yield data.delta.text;
|
|
207
|
+
} else if (data.content) {
|
|
179
208
|
yield data.content;
|
|
180
209
|
} else if (data.text) {
|
|
181
210
|
yield data.text;
|
|
182
|
-
} else if (data.delta?.text) {
|
|
183
|
-
yield data.delta.text;
|
|
184
211
|
}
|
|
185
212
|
} catch {
|
|
186
213
|
if (!trimmed.startsWith("{")) {
|
|
@@ -193,6 +220,13 @@ var ClaudeCLI = class {
|
|
|
193
220
|
};
|
|
194
221
|
|
|
195
222
|
// src/server.ts
|
|
223
|
+
var _verbose = false;
|
|
224
|
+
function log(...args) {
|
|
225
|
+
if (_verbose) {
|
|
226
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
227
|
+
console.log(`[${timestamp}]`, ...args);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
196
230
|
var _cli = null;
|
|
197
231
|
function sendJson(res, data, status = 200) {
|
|
198
232
|
const body = JSON.stringify(data);
|
|
@@ -263,6 +297,9 @@ async function handleRequest(req, res) {
|
|
|
263
297
|
}
|
|
264
298
|
const chatId = generateId("chatcmpl");
|
|
265
299
|
const created = Math.floor(Date.now() / 1e3);
|
|
300
|
+
const msgCount = request.messages?.length ?? 0;
|
|
301
|
+
const lastMsg = request.messages?.[msgCount - 1]?.content?.slice(0, 30) ?? "";
|
|
302
|
+
log(`\u2192 POST /v1/chat/completions model=${request.model} stream=${!!request.stream} msgs=${msgCount} "${lastMsg}${lastMsg.length >= 30 ? "..." : ""}"`);
|
|
266
303
|
if (request.stream) {
|
|
267
304
|
res.writeHead(200, {
|
|
268
305
|
"Content-Type": "text/event-stream",
|
|
@@ -279,8 +316,10 @@ async function handleRequest(req, res) {
|
|
|
279
316
|
choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }]
|
|
280
317
|
};
|
|
281
318
|
sendSSEChunk(res, JSON.stringify(initial));
|
|
319
|
+
let totalLen = 0;
|
|
282
320
|
try {
|
|
283
321
|
for await (const text of _cli.stream(request.messages, request.model)) {
|
|
322
|
+
totalLen += text.length;
|
|
284
323
|
const chunk = {
|
|
285
324
|
id: chatId,
|
|
286
325
|
object: "chat.completion.chunk",
|
|
@@ -290,7 +329,9 @@ async function handleRequest(req, res) {
|
|
|
290
329
|
};
|
|
291
330
|
sendSSEChunk(res, JSON.stringify(chunk));
|
|
292
331
|
}
|
|
332
|
+
log(`\u2190 stream complete, total length=${totalLen}`);
|
|
293
333
|
} catch (err) {
|
|
334
|
+
log(`\u2190 stream error: ${err}`);
|
|
294
335
|
sendSSEChunk(res, JSON.stringify({ error: { message: String(err), type: "server_error" } }));
|
|
295
336
|
}
|
|
296
337
|
const final = {
|
|
@@ -307,6 +348,7 @@ async function handleRequest(req, res) {
|
|
|
307
348
|
}
|
|
308
349
|
try {
|
|
309
350
|
const content = await _cli.complete(request.messages, request.model);
|
|
351
|
+
log(`\u2190 response complete, length=${content.length}`);
|
|
310
352
|
const response = {
|
|
311
353
|
id: chatId,
|
|
312
354
|
object: "chat.completion",
|
|
@@ -317,6 +359,7 @@ async function handleRequest(req, res) {
|
|
|
317
359
|
};
|
|
318
360
|
sendJson(res, response);
|
|
319
361
|
} catch (err) {
|
|
362
|
+
log(`\u2190 error: ${err}`);
|
|
320
363
|
sendError(res, String(err));
|
|
321
364
|
}
|
|
322
365
|
return;
|
|
@@ -324,8 +367,9 @@ async function handleRequest(req, res) {
|
|
|
324
367
|
sendError(res, "Not found", 404);
|
|
325
368
|
}
|
|
326
369
|
function createApp(config) {
|
|
370
|
+
_verbose = config?.verbose ?? process.env.CLAUDE_CODE_RELAY_VERBOSE === "1";
|
|
327
371
|
try {
|
|
328
|
-
_cli = new ClaudeCLI(config?.cli);
|
|
372
|
+
_cli = new ClaudeCLI({ ...config?.cli, verbose: _verbose });
|
|
329
373
|
} catch (err) {
|
|
330
374
|
console.error(`Failed to initialize Claude CLI: ${err}`);
|
|
331
375
|
_cli = null;
|
package/dist/cjs/server.js
CHANGED
|
@@ -118,8 +118,10 @@ var ClaudeCLI = class {
|
|
|
118
118
|
const normalizedModel = this.normalizeModel(model);
|
|
119
119
|
return new Promise((resolve, reject) => {
|
|
120
120
|
const args = ["-p", "--model", normalizedModel, "--output-format", "text"];
|
|
121
|
+
const startTime = Date.now();
|
|
121
122
|
if (this.config.verbose) {
|
|
122
|
-
console.log(`
|
|
123
|
+
console.log(`[claude] spawning: ${this.config.cliPath} ${args.join(" ")}`);
|
|
124
|
+
console.log(`[claude] prompt length: ${prompt.length} chars`);
|
|
123
125
|
}
|
|
124
126
|
const proc = (0, import_node_child_process2.spawn)(this.config.cliPath, args, {
|
|
125
127
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -133,6 +135,10 @@ var ClaudeCLI = class {
|
|
|
133
135
|
stderr += data.toString();
|
|
134
136
|
});
|
|
135
137
|
proc.on("close", (code) => {
|
|
138
|
+
const elapsed = Date.now() - startTime;
|
|
139
|
+
if (this.config.verbose) {
|
|
140
|
+
console.log(`[claude] process exited code=${code} elapsed=${elapsed}ms output=${stdout.length} chars`);
|
|
141
|
+
}
|
|
136
142
|
if (code !== 0) {
|
|
137
143
|
reject(new Error(`Claude CLI failed: ${stderr}`));
|
|
138
144
|
} else {
|
|
@@ -153,18 +159,30 @@ var ClaudeCLI = class {
|
|
|
153
159
|
async *stream(messages, model = "sonnet", systemPrompt) {
|
|
154
160
|
const prompt = this.buildPrompt(messages, systemPrompt);
|
|
155
161
|
const normalizedModel = this.normalizeModel(model);
|
|
156
|
-
const
|
|
162
|
+
const startTime = Date.now();
|
|
163
|
+
const args = ["-p", "--verbose", "--model", normalizedModel, "--output-format", "stream-json"];
|
|
157
164
|
if (this.config.verbose) {
|
|
158
|
-
console.log(`
|
|
165
|
+
console.log(`[claude] spawning stream: ${this.config.cliPath} ${args.join(" ")}`);
|
|
166
|
+
console.log(`[claude] prompt length: ${prompt.length} chars`);
|
|
159
167
|
}
|
|
160
168
|
const proc = (0, import_node_child_process2.spawn)(this.config.cliPath, args, {
|
|
161
169
|
stdio: ["pipe", "pipe", "pipe"]
|
|
162
170
|
});
|
|
171
|
+
proc.on("close", (code) => {
|
|
172
|
+
if (this.config.verbose) {
|
|
173
|
+
const elapsed = Date.now() - startTime;
|
|
174
|
+
console.log(`[claude] stream process exited code=${code} elapsed=${elapsed}ms`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
163
177
|
proc.stdin.write(prompt);
|
|
164
178
|
proc.stdin.end();
|
|
165
179
|
let buffer = "";
|
|
166
180
|
for await (const chunk of proc.stdout) {
|
|
167
|
-
|
|
181
|
+
const chunkStr = chunk.toString();
|
|
182
|
+
buffer += chunkStr;
|
|
183
|
+
if (this.config.verbose) {
|
|
184
|
+
console.log(`[claude] stdout chunk: ${chunkStr.length} bytes`);
|
|
185
|
+
}
|
|
168
186
|
while (buffer.includes("\n")) {
|
|
169
187
|
const [line, rest] = buffer.split("\n", 2);
|
|
170
188
|
buffer = rest ?? "";
|
|
@@ -172,12 +190,21 @@ var ClaudeCLI = class {
|
|
|
172
190
|
if (!trimmed) continue;
|
|
173
191
|
try {
|
|
174
192
|
const data = JSON.parse(trimmed);
|
|
175
|
-
if (
|
|
193
|
+
if (this.config.verbose) {
|
|
194
|
+
console.log(`[claude] parsed type=${data.type}`);
|
|
195
|
+
}
|
|
196
|
+
if (data.type === "assistant" && data.message?.content) {
|
|
197
|
+
for (const block of data.message.content) {
|
|
198
|
+
if (block.type === "text" && block.text) {
|
|
199
|
+
yield block.text;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} else if (data.type === "content_block_delta" && data.delta?.text) {
|
|
203
|
+
yield data.delta.text;
|
|
204
|
+
} else if (data.content) {
|
|
176
205
|
yield data.content;
|
|
177
206
|
} else if (data.text) {
|
|
178
207
|
yield data.text;
|
|
179
|
-
} else if (data.delta?.text) {
|
|
180
|
-
yield data.delta.text;
|
|
181
208
|
}
|
|
182
209
|
} catch {
|
|
183
210
|
if (!trimmed.startsWith("{")) {
|
|
@@ -190,6 +217,13 @@ var ClaudeCLI = class {
|
|
|
190
217
|
};
|
|
191
218
|
|
|
192
219
|
// src/server.ts
|
|
220
|
+
var _verbose = false;
|
|
221
|
+
function log(...args) {
|
|
222
|
+
if (_verbose) {
|
|
223
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
224
|
+
console.log(`[${timestamp}]`, ...args);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
193
227
|
var _cli = null;
|
|
194
228
|
function sendJson(res, data, status = 200) {
|
|
195
229
|
const body = JSON.stringify(data);
|
|
@@ -260,6 +294,9 @@ async function handleRequest(req, res) {
|
|
|
260
294
|
}
|
|
261
295
|
const chatId = generateId("chatcmpl");
|
|
262
296
|
const created = Math.floor(Date.now() / 1e3);
|
|
297
|
+
const msgCount = request.messages?.length ?? 0;
|
|
298
|
+
const lastMsg = request.messages?.[msgCount - 1]?.content?.slice(0, 30) ?? "";
|
|
299
|
+
log(`\u2192 POST /v1/chat/completions model=${request.model} stream=${!!request.stream} msgs=${msgCount} "${lastMsg}${lastMsg.length >= 30 ? "..." : ""}"`);
|
|
263
300
|
if (request.stream) {
|
|
264
301
|
res.writeHead(200, {
|
|
265
302
|
"Content-Type": "text/event-stream",
|
|
@@ -276,8 +313,10 @@ async function handleRequest(req, res) {
|
|
|
276
313
|
choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }]
|
|
277
314
|
};
|
|
278
315
|
sendSSEChunk(res, JSON.stringify(initial));
|
|
316
|
+
let totalLen = 0;
|
|
279
317
|
try {
|
|
280
318
|
for await (const text of _cli.stream(request.messages, request.model)) {
|
|
319
|
+
totalLen += text.length;
|
|
281
320
|
const chunk = {
|
|
282
321
|
id: chatId,
|
|
283
322
|
object: "chat.completion.chunk",
|
|
@@ -287,7 +326,9 @@ async function handleRequest(req, res) {
|
|
|
287
326
|
};
|
|
288
327
|
sendSSEChunk(res, JSON.stringify(chunk));
|
|
289
328
|
}
|
|
329
|
+
log(`\u2190 stream complete, total length=${totalLen}`);
|
|
290
330
|
} catch (err) {
|
|
331
|
+
log(`\u2190 stream error: ${err}`);
|
|
291
332
|
sendSSEChunk(res, JSON.stringify({ error: { message: String(err), type: "server_error" } }));
|
|
292
333
|
}
|
|
293
334
|
const final = {
|
|
@@ -304,6 +345,7 @@ async function handleRequest(req, res) {
|
|
|
304
345
|
}
|
|
305
346
|
try {
|
|
306
347
|
const content = await _cli.complete(request.messages, request.model);
|
|
348
|
+
log(`\u2190 response complete, length=${content.length}`);
|
|
307
349
|
const response = {
|
|
308
350
|
id: chatId,
|
|
309
351
|
object: "chat.completion",
|
|
@@ -314,6 +356,7 @@ async function handleRequest(req, res) {
|
|
|
314
356
|
};
|
|
315
357
|
sendJson(res, response);
|
|
316
358
|
} catch (err) {
|
|
359
|
+
log(`\u2190 error: ${err}`);
|
|
317
360
|
sendError(res, String(err));
|
|
318
361
|
}
|
|
319
362
|
return;
|
|
@@ -321,8 +364,9 @@ async function handleRequest(req, res) {
|
|
|
321
364
|
sendError(res, "Not found", 404);
|
|
322
365
|
}
|
|
323
366
|
function createApp(config) {
|
|
367
|
+
_verbose = config?.verbose ?? process.env.CLAUDE_CODE_RELAY_VERBOSE === "1";
|
|
324
368
|
try {
|
|
325
|
-
_cli = new ClaudeCLI(config?.cli);
|
|
369
|
+
_cli = new ClaudeCLI({ ...config?.cli, verbose: _verbose });
|
|
326
370
|
} catch (err) {
|
|
327
371
|
console.error(`Failed to initialize Claude CLI: ${err}`);
|
|
328
372
|
_cli = null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-wrapper.d.ts","sourceRoot":"","sources":["../../src/cli-wrapper.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AAaD,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAY;gBAEd,MAAM,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC;IAUvC,OAAO,CAAC,WAAW;IAanB,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,WAAW;IA4Bb,QAAQ,CACZ,QAAQ,EAAE,WAAW,EAAE,EACvB,KAAK,SAAW,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"cli-wrapper.d.ts","sourceRoot":"","sources":["../../src/cli-wrapper.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AAaD,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAY;gBAEd,MAAM,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC;IAUvC,OAAO,CAAC,WAAW;IAanB,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,WAAW;IA4Bb,QAAQ,CACZ,QAAQ,EAAE,WAAW,EAAE,EACvB,KAAK,SAAW,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC;IAwDX,MAAM,CACX,QAAQ,EAAE,WAAW,EAAE,EACvB,KAAK,SAAW,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC;CAyEzC"}
|
package/dist/esm/cli-wrapper.js
CHANGED
|
@@ -86,8 +86,10 @@ var ClaudeCLI = class {
|
|
|
86
86
|
const normalizedModel = this.normalizeModel(model);
|
|
87
87
|
return new Promise((resolve, reject) => {
|
|
88
88
|
const args = ["-p", "--model", normalizedModel, "--output-format", "text"];
|
|
89
|
+
const startTime = Date.now();
|
|
89
90
|
if (this.config.verbose) {
|
|
90
|
-
console.log(`
|
|
91
|
+
console.log(`[claude] spawning: ${this.config.cliPath} ${args.join(" ")}`);
|
|
92
|
+
console.log(`[claude] prompt length: ${prompt.length} chars`);
|
|
91
93
|
}
|
|
92
94
|
const proc = spawn(this.config.cliPath, args, {
|
|
93
95
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -101,6 +103,10 @@ var ClaudeCLI = class {
|
|
|
101
103
|
stderr += data.toString();
|
|
102
104
|
});
|
|
103
105
|
proc.on("close", (code) => {
|
|
106
|
+
const elapsed = Date.now() - startTime;
|
|
107
|
+
if (this.config.verbose) {
|
|
108
|
+
console.log(`[claude] process exited code=${code} elapsed=${elapsed}ms output=${stdout.length} chars`);
|
|
109
|
+
}
|
|
104
110
|
if (code !== 0) {
|
|
105
111
|
reject(new Error(`Claude CLI failed: ${stderr}`));
|
|
106
112
|
} else {
|
|
@@ -121,18 +127,30 @@ var ClaudeCLI = class {
|
|
|
121
127
|
async *stream(messages, model = "sonnet", systemPrompt) {
|
|
122
128
|
const prompt = this.buildPrompt(messages, systemPrompt);
|
|
123
129
|
const normalizedModel = this.normalizeModel(model);
|
|
124
|
-
const
|
|
130
|
+
const startTime = Date.now();
|
|
131
|
+
const args = ["-p", "--verbose", "--model", normalizedModel, "--output-format", "stream-json"];
|
|
125
132
|
if (this.config.verbose) {
|
|
126
|
-
console.log(`
|
|
133
|
+
console.log(`[claude] spawning stream: ${this.config.cliPath} ${args.join(" ")}`);
|
|
134
|
+
console.log(`[claude] prompt length: ${prompt.length} chars`);
|
|
127
135
|
}
|
|
128
136
|
const proc = spawn(this.config.cliPath, args, {
|
|
129
137
|
stdio: ["pipe", "pipe", "pipe"]
|
|
130
138
|
});
|
|
139
|
+
proc.on("close", (code) => {
|
|
140
|
+
if (this.config.verbose) {
|
|
141
|
+
const elapsed = Date.now() - startTime;
|
|
142
|
+
console.log(`[claude] stream process exited code=${code} elapsed=${elapsed}ms`);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
131
145
|
proc.stdin.write(prompt);
|
|
132
146
|
proc.stdin.end();
|
|
133
147
|
let buffer = "";
|
|
134
148
|
for await (const chunk of proc.stdout) {
|
|
135
|
-
|
|
149
|
+
const chunkStr = chunk.toString();
|
|
150
|
+
buffer += chunkStr;
|
|
151
|
+
if (this.config.verbose) {
|
|
152
|
+
console.log(`[claude] stdout chunk: ${chunkStr.length} bytes`);
|
|
153
|
+
}
|
|
136
154
|
while (buffer.includes("\n")) {
|
|
137
155
|
const [line, rest] = buffer.split("\n", 2);
|
|
138
156
|
buffer = rest ?? "";
|
|
@@ -140,12 +158,21 @@ var ClaudeCLI = class {
|
|
|
140
158
|
if (!trimmed) continue;
|
|
141
159
|
try {
|
|
142
160
|
const data = JSON.parse(trimmed);
|
|
143
|
-
if (
|
|
161
|
+
if (this.config.verbose) {
|
|
162
|
+
console.log(`[claude] parsed type=${data.type}`);
|
|
163
|
+
}
|
|
164
|
+
if (data.type === "assistant" && data.message?.content) {
|
|
165
|
+
for (const block of data.message.content) {
|
|
166
|
+
if (block.type === "text" && block.text) {
|
|
167
|
+
yield block.text;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} else if (data.type === "content_block_delta" && data.delta?.text) {
|
|
171
|
+
yield data.delta.text;
|
|
172
|
+
} else if (data.content) {
|
|
144
173
|
yield data.content;
|
|
145
174
|
} else if (data.text) {
|
|
146
175
|
yield data.text;
|
|
147
|
-
} else if (data.delta?.text) {
|
|
148
|
-
yield data.delta.text;
|
|
149
176
|
}
|
|
150
177
|
} catch {
|
|
151
178
|
if (!trimmed.startsWith("{")) {
|
package/dist/esm/cli.js
CHANGED
|
@@ -98,8 +98,10 @@ var ClaudeCLI = class {
|
|
|
98
98
|
const normalizedModel = this.normalizeModel(model);
|
|
99
99
|
return new Promise((resolve, reject) => {
|
|
100
100
|
const args = ["-p", "--model", normalizedModel, "--output-format", "text"];
|
|
101
|
+
const startTime = Date.now();
|
|
101
102
|
if (this.config.verbose) {
|
|
102
|
-
console.log(`
|
|
103
|
+
console.log(`[claude] spawning: ${this.config.cliPath} ${args.join(" ")}`);
|
|
104
|
+
console.log(`[claude] prompt length: ${prompt.length} chars`);
|
|
103
105
|
}
|
|
104
106
|
const proc = spawn(this.config.cliPath, args, {
|
|
105
107
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -113,6 +115,10 @@ var ClaudeCLI = class {
|
|
|
113
115
|
stderr += data.toString();
|
|
114
116
|
});
|
|
115
117
|
proc.on("close", (code) => {
|
|
118
|
+
const elapsed = Date.now() - startTime;
|
|
119
|
+
if (this.config.verbose) {
|
|
120
|
+
console.log(`[claude] process exited code=${code} elapsed=${elapsed}ms output=${stdout.length} chars`);
|
|
121
|
+
}
|
|
116
122
|
if (code !== 0) {
|
|
117
123
|
reject(new Error(`Claude CLI failed: ${stderr}`));
|
|
118
124
|
} else {
|
|
@@ -133,18 +139,30 @@ var ClaudeCLI = class {
|
|
|
133
139
|
async *stream(messages, model = "sonnet", systemPrompt) {
|
|
134
140
|
const prompt = this.buildPrompt(messages, systemPrompt);
|
|
135
141
|
const normalizedModel = this.normalizeModel(model);
|
|
136
|
-
const
|
|
142
|
+
const startTime = Date.now();
|
|
143
|
+
const args = ["-p", "--verbose", "--model", normalizedModel, "--output-format", "stream-json"];
|
|
137
144
|
if (this.config.verbose) {
|
|
138
|
-
console.log(`
|
|
145
|
+
console.log(`[claude] spawning stream: ${this.config.cliPath} ${args.join(" ")}`);
|
|
146
|
+
console.log(`[claude] prompt length: ${prompt.length} chars`);
|
|
139
147
|
}
|
|
140
148
|
const proc = spawn(this.config.cliPath, args, {
|
|
141
149
|
stdio: ["pipe", "pipe", "pipe"]
|
|
142
150
|
});
|
|
151
|
+
proc.on("close", (code) => {
|
|
152
|
+
if (this.config.verbose) {
|
|
153
|
+
const elapsed = Date.now() - startTime;
|
|
154
|
+
console.log(`[claude] stream process exited code=${code} elapsed=${elapsed}ms`);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
143
157
|
proc.stdin.write(prompt);
|
|
144
158
|
proc.stdin.end();
|
|
145
159
|
let buffer = "";
|
|
146
160
|
for await (const chunk of proc.stdout) {
|
|
147
|
-
|
|
161
|
+
const chunkStr = chunk.toString();
|
|
162
|
+
buffer += chunkStr;
|
|
163
|
+
if (this.config.verbose) {
|
|
164
|
+
console.log(`[claude] stdout chunk: ${chunkStr.length} bytes`);
|
|
165
|
+
}
|
|
148
166
|
while (buffer.includes("\n")) {
|
|
149
167
|
const [line, rest] = buffer.split("\n", 2);
|
|
150
168
|
buffer = rest ?? "";
|
|
@@ -152,12 +170,21 @@ var ClaudeCLI = class {
|
|
|
152
170
|
if (!trimmed) continue;
|
|
153
171
|
try {
|
|
154
172
|
const data = JSON.parse(trimmed);
|
|
155
|
-
if (
|
|
173
|
+
if (this.config.verbose) {
|
|
174
|
+
console.log(`[claude] parsed type=${data.type}`);
|
|
175
|
+
}
|
|
176
|
+
if (data.type === "assistant" && data.message?.content) {
|
|
177
|
+
for (const block of data.message.content) {
|
|
178
|
+
if (block.type === "text" && block.text) {
|
|
179
|
+
yield block.text;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} else if (data.type === "content_block_delta" && data.delta?.text) {
|
|
183
|
+
yield data.delta.text;
|
|
184
|
+
} else if (data.content) {
|
|
156
185
|
yield data.content;
|
|
157
186
|
} else if (data.text) {
|
|
158
187
|
yield data.text;
|
|
159
|
-
} else if (data.delta?.text) {
|
|
160
|
-
yield data.delta.text;
|
|
161
188
|
}
|
|
162
189
|
} catch {
|
|
163
190
|
if (!trimmed.startsWith("{")) {
|
|
@@ -170,6 +197,13 @@ var ClaudeCLI = class {
|
|
|
170
197
|
};
|
|
171
198
|
|
|
172
199
|
// src/server.ts
|
|
200
|
+
var _verbose = false;
|
|
201
|
+
function log(...args) {
|
|
202
|
+
if (_verbose) {
|
|
203
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
204
|
+
console.log(`[${timestamp}]`, ...args);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
173
207
|
var _cli = null;
|
|
174
208
|
function sendJson(res, data, status = 200) {
|
|
175
209
|
const body = JSON.stringify(data);
|
|
@@ -240,6 +274,9 @@ async function handleRequest(req, res) {
|
|
|
240
274
|
}
|
|
241
275
|
const chatId = generateId("chatcmpl");
|
|
242
276
|
const created = Math.floor(Date.now() / 1e3);
|
|
277
|
+
const msgCount = request.messages?.length ?? 0;
|
|
278
|
+
const lastMsg = request.messages?.[msgCount - 1]?.content?.slice(0, 30) ?? "";
|
|
279
|
+
log(`\u2192 POST /v1/chat/completions model=${request.model} stream=${!!request.stream} msgs=${msgCount} "${lastMsg}${lastMsg.length >= 30 ? "..." : ""}"`);
|
|
243
280
|
if (request.stream) {
|
|
244
281
|
res.writeHead(200, {
|
|
245
282
|
"Content-Type": "text/event-stream",
|
|
@@ -256,8 +293,10 @@ async function handleRequest(req, res) {
|
|
|
256
293
|
choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }]
|
|
257
294
|
};
|
|
258
295
|
sendSSEChunk(res, JSON.stringify(initial));
|
|
296
|
+
let totalLen = 0;
|
|
259
297
|
try {
|
|
260
298
|
for await (const text of _cli.stream(request.messages, request.model)) {
|
|
299
|
+
totalLen += text.length;
|
|
261
300
|
const chunk = {
|
|
262
301
|
id: chatId,
|
|
263
302
|
object: "chat.completion.chunk",
|
|
@@ -267,7 +306,9 @@ async function handleRequest(req, res) {
|
|
|
267
306
|
};
|
|
268
307
|
sendSSEChunk(res, JSON.stringify(chunk));
|
|
269
308
|
}
|
|
309
|
+
log(`\u2190 stream complete, total length=${totalLen}`);
|
|
270
310
|
} catch (err) {
|
|
311
|
+
log(`\u2190 stream error: ${err}`);
|
|
271
312
|
sendSSEChunk(res, JSON.stringify({ error: { message: String(err), type: "server_error" } }));
|
|
272
313
|
}
|
|
273
314
|
const final = {
|
|
@@ -284,6 +325,7 @@ async function handleRequest(req, res) {
|
|
|
284
325
|
}
|
|
285
326
|
try {
|
|
286
327
|
const content = await _cli.complete(request.messages, request.model);
|
|
328
|
+
log(`\u2190 response complete, length=${content.length}`);
|
|
287
329
|
const response = {
|
|
288
330
|
id: chatId,
|
|
289
331
|
object: "chat.completion",
|
|
@@ -294,6 +336,7 @@ async function handleRequest(req, res) {
|
|
|
294
336
|
};
|
|
295
337
|
sendJson(res, response);
|
|
296
338
|
} catch (err) {
|
|
339
|
+
log(`\u2190 error: ${err}`);
|
|
297
340
|
sendError(res, String(err));
|
|
298
341
|
}
|
|
299
342
|
return;
|
|
@@ -301,8 +344,9 @@ async function handleRequest(req, res) {
|
|
|
301
344
|
sendError(res, "Not found", 404);
|
|
302
345
|
}
|
|
303
346
|
function createApp(config) {
|
|
347
|
+
_verbose = config?.verbose ?? process.env.CLAUDE_CODE_RELAY_VERBOSE === "1";
|
|
304
348
|
try {
|
|
305
|
-
_cli = new ClaudeCLI(config?.cli);
|
|
349
|
+
_cli = new ClaudeCLI({ ...config?.cli, verbose: _verbose });
|
|
306
350
|
} catch (err) {
|
|
307
351
|
console.error(`Failed to initialize Claude CLI: ${err}`);
|
|
308
352
|
_cli = null;
|
|
@@ -375,11 +419,13 @@ if (command === "serve") {
|
|
|
375
419
|
console.log(` Port: ${port}`);
|
|
376
420
|
console.log(` Claude CLI: ${claudePath}`);
|
|
377
421
|
console.log(` Timeout: ${timeout}s`);
|
|
422
|
+
console.log(` Verbose: ${verbose}`);
|
|
378
423
|
console.log();
|
|
379
424
|
console.log(`API endpoint: http://${host}:${port}/v1/chat/completions`);
|
|
380
425
|
console.log();
|
|
381
426
|
runServer(host, port, {
|
|
382
|
-
cli: { cliPath: claudePath, timeout, verbose }
|
|
427
|
+
cli: { cliPath: claudePath, timeout, verbose },
|
|
428
|
+
verbose
|
|
383
429
|
});
|
|
384
430
|
} else if (command === "check") {
|
|
385
431
|
console.log("Checking Claude CLI...");
|
package/dist/esm/index.js
CHANGED
|
@@ -93,8 +93,10 @@ var ClaudeCLI = class {
|
|
|
93
93
|
const normalizedModel = this.normalizeModel(model);
|
|
94
94
|
return new Promise((resolve, reject) => {
|
|
95
95
|
const args = ["-p", "--model", normalizedModel, "--output-format", "text"];
|
|
96
|
+
const startTime = Date.now();
|
|
96
97
|
if (this.config.verbose) {
|
|
97
|
-
console.log(`
|
|
98
|
+
console.log(`[claude] spawning: ${this.config.cliPath} ${args.join(" ")}`);
|
|
99
|
+
console.log(`[claude] prompt length: ${prompt.length} chars`);
|
|
98
100
|
}
|
|
99
101
|
const proc = spawn(this.config.cliPath, args, {
|
|
100
102
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -108,6 +110,10 @@ var ClaudeCLI = class {
|
|
|
108
110
|
stderr += data.toString();
|
|
109
111
|
});
|
|
110
112
|
proc.on("close", (code) => {
|
|
113
|
+
const elapsed = Date.now() - startTime;
|
|
114
|
+
if (this.config.verbose) {
|
|
115
|
+
console.log(`[claude] process exited code=${code} elapsed=${elapsed}ms output=${stdout.length} chars`);
|
|
116
|
+
}
|
|
111
117
|
if (code !== 0) {
|
|
112
118
|
reject(new Error(`Claude CLI failed: ${stderr}`));
|
|
113
119
|
} else {
|
|
@@ -128,18 +134,30 @@ var ClaudeCLI = class {
|
|
|
128
134
|
async *stream(messages, model = "sonnet", systemPrompt) {
|
|
129
135
|
const prompt = this.buildPrompt(messages, systemPrompt);
|
|
130
136
|
const normalizedModel = this.normalizeModel(model);
|
|
131
|
-
const
|
|
137
|
+
const startTime = Date.now();
|
|
138
|
+
const args = ["-p", "--verbose", "--model", normalizedModel, "--output-format", "stream-json"];
|
|
132
139
|
if (this.config.verbose) {
|
|
133
|
-
console.log(`
|
|
140
|
+
console.log(`[claude] spawning stream: ${this.config.cliPath} ${args.join(" ")}`);
|
|
141
|
+
console.log(`[claude] prompt length: ${prompt.length} chars`);
|
|
134
142
|
}
|
|
135
143
|
const proc = spawn(this.config.cliPath, args, {
|
|
136
144
|
stdio: ["pipe", "pipe", "pipe"]
|
|
137
145
|
});
|
|
146
|
+
proc.on("close", (code) => {
|
|
147
|
+
if (this.config.verbose) {
|
|
148
|
+
const elapsed = Date.now() - startTime;
|
|
149
|
+
console.log(`[claude] stream process exited code=${code} elapsed=${elapsed}ms`);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
138
152
|
proc.stdin.write(prompt);
|
|
139
153
|
proc.stdin.end();
|
|
140
154
|
let buffer = "";
|
|
141
155
|
for await (const chunk of proc.stdout) {
|
|
142
|
-
|
|
156
|
+
const chunkStr = chunk.toString();
|
|
157
|
+
buffer += chunkStr;
|
|
158
|
+
if (this.config.verbose) {
|
|
159
|
+
console.log(`[claude] stdout chunk: ${chunkStr.length} bytes`);
|
|
160
|
+
}
|
|
143
161
|
while (buffer.includes("\n")) {
|
|
144
162
|
const [line, rest] = buffer.split("\n", 2);
|
|
145
163
|
buffer = rest ?? "";
|
|
@@ -147,12 +165,21 @@ var ClaudeCLI = class {
|
|
|
147
165
|
if (!trimmed) continue;
|
|
148
166
|
try {
|
|
149
167
|
const data = JSON.parse(trimmed);
|
|
150
|
-
if (
|
|
168
|
+
if (this.config.verbose) {
|
|
169
|
+
console.log(`[claude] parsed type=${data.type}`);
|
|
170
|
+
}
|
|
171
|
+
if (data.type === "assistant" && data.message?.content) {
|
|
172
|
+
for (const block of data.message.content) {
|
|
173
|
+
if (block.type === "text" && block.text) {
|
|
174
|
+
yield block.text;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} else if (data.type === "content_block_delta" && data.delta?.text) {
|
|
178
|
+
yield data.delta.text;
|
|
179
|
+
} else if (data.content) {
|
|
151
180
|
yield data.content;
|
|
152
181
|
} else if (data.text) {
|
|
153
182
|
yield data.text;
|
|
154
|
-
} else if (data.delta?.text) {
|
|
155
|
-
yield data.delta.text;
|
|
156
183
|
}
|
|
157
184
|
} catch {
|
|
158
185
|
if (!trimmed.startsWith("{")) {
|
|
@@ -165,6 +192,13 @@ var ClaudeCLI = class {
|
|
|
165
192
|
};
|
|
166
193
|
|
|
167
194
|
// src/server.ts
|
|
195
|
+
var _verbose = false;
|
|
196
|
+
function log(...args) {
|
|
197
|
+
if (_verbose) {
|
|
198
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
199
|
+
console.log(`[${timestamp}]`, ...args);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
168
202
|
var _cli = null;
|
|
169
203
|
function sendJson(res, data, status = 200) {
|
|
170
204
|
const body = JSON.stringify(data);
|
|
@@ -235,6 +269,9 @@ async function handleRequest(req, res) {
|
|
|
235
269
|
}
|
|
236
270
|
const chatId = generateId("chatcmpl");
|
|
237
271
|
const created = Math.floor(Date.now() / 1e3);
|
|
272
|
+
const msgCount = request.messages?.length ?? 0;
|
|
273
|
+
const lastMsg = request.messages?.[msgCount - 1]?.content?.slice(0, 30) ?? "";
|
|
274
|
+
log(`\u2192 POST /v1/chat/completions model=${request.model} stream=${!!request.stream} msgs=${msgCount} "${lastMsg}${lastMsg.length >= 30 ? "..." : ""}"`);
|
|
238
275
|
if (request.stream) {
|
|
239
276
|
res.writeHead(200, {
|
|
240
277
|
"Content-Type": "text/event-stream",
|
|
@@ -251,8 +288,10 @@ async function handleRequest(req, res) {
|
|
|
251
288
|
choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }]
|
|
252
289
|
};
|
|
253
290
|
sendSSEChunk(res, JSON.stringify(initial));
|
|
291
|
+
let totalLen = 0;
|
|
254
292
|
try {
|
|
255
293
|
for await (const text of _cli.stream(request.messages, request.model)) {
|
|
294
|
+
totalLen += text.length;
|
|
256
295
|
const chunk = {
|
|
257
296
|
id: chatId,
|
|
258
297
|
object: "chat.completion.chunk",
|
|
@@ -262,7 +301,9 @@ async function handleRequest(req, res) {
|
|
|
262
301
|
};
|
|
263
302
|
sendSSEChunk(res, JSON.stringify(chunk));
|
|
264
303
|
}
|
|
304
|
+
log(`\u2190 stream complete, total length=${totalLen}`);
|
|
265
305
|
} catch (err) {
|
|
306
|
+
log(`\u2190 stream error: ${err}`);
|
|
266
307
|
sendSSEChunk(res, JSON.stringify({ error: { message: String(err), type: "server_error" } }));
|
|
267
308
|
}
|
|
268
309
|
const final = {
|
|
@@ -279,6 +320,7 @@ async function handleRequest(req, res) {
|
|
|
279
320
|
}
|
|
280
321
|
try {
|
|
281
322
|
const content = await _cli.complete(request.messages, request.model);
|
|
323
|
+
log(`\u2190 response complete, length=${content.length}`);
|
|
282
324
|
const response = {
|
|
283
325
|
id: chatId,
|
|
284
326
|
object: "chat.completion",
|
|
@@ -289,6 +331,7 @@ async function handleRequest(req, res) {
|
|
|
289
331
|
};
|
|
290
332
|
sendJson(res, response);
|
|
291
333
|
} catch (err) {
|
|
334
|
+
log(`\u2190 error: ${err}`);
|
|
292
335
|
sendError(res, String(err));
|
|
293
336
|
}
|
|
294
337
|
return;
|
|
@@ -296,8 +339,9 @@ async function handleRequest(req, res) {
|
|
|
296
339
|
sendError(res, "Not found", 404);
|
|
297
340
|
}
|
|
298
341
|
function createApp(config) {
|
|
342
|
+
_verbose = config?.verbose ?? process.env.CLAUDE_CODE_RELAY_VERBOSE === "1";
|
|
299
343
|
try {
|
|
300
|
-
_cli = new ClaudeCLI(config?.cli);
|
|
344
|
+
_cli = new ClaudeCLI({ ...config?.cli, verbose: _verbose });
|
|
301
345
|
} catch (err) {
|
|
302
346
|
console.error(`Failed to initialize Claude CLI: ${err}`);
|
|
303
347
|
_cli = null;
|
package/dist/esm/server.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { IncomingMessage, ServerResponse } from "node:http";
|
|
|
5
5
|
import { type CLIConfig } from "./cli-wrapper.js";
|
|
6
6
|
export interface AppConfig {
|
|
7
7
|
cli?: Partial<CLIConfig>;
|
|
8
|
+
verbose?: boolean;
|
|
8
9
|
}
|
|
9
10
|
export declare function createApp(config?: AppConfig): import("node:http").Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
10
11
|
export declare function runServer(host?: string, port?: number, config?: AppConfig): void;
|
package/dist/esm/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAgB,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC1E,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAS7D,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAgB,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC1E,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAS7D,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA+KD,wBAAgB,SAAS,CAAC,MAAM,CAAC,EAAE,SAAS,6EAgB3C;AAED,wBAAgB,SAAS,CAAC,IAAI,SAAc,EAAE,IAAI,SAAQ,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,IAAI,CAKpF"}
|
package/dist/esm/server.js
CHANGED
|
@@ -93,8 +93,10 @@ var ClaudeCLI = class {
|
|
|
93
93
|
const normalizedModel = this.normalizeModel(model);
|
|
94
94
|
return new Promise((resolve, reject) => {
|
|
95
95
|
const args = ["-p", "--model", normalizedModel, "--output-format", "text"];
|
|
96
|
+
const startTime = Date.now();
|
|
96
97
|
if (this.config.verbose) {
|
|
97
|
-
console.log(`
|
|
98
|
+
console.log(`[claude] spawning: ${this.config.cliPath} ${args.join(" ")}`);
|
|
99
|
+
console.log(`[claude] prompt length: ${prompt.length} chars`);
|
|
98
100
|
}
|
|
99
101
|
const proc = spawn(this.config.cliPath, args, {
|
|
100
102
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -108,6 +110,10 @@ var ClaudeCLI = class {
|
|
|
108
110
|
stderr += data.toString();
|
|
109
111
|
});
|
|
110
112
|
proc.on("close", (code) => {
|
|
113
|
+
const elapsed = Date.now() - startTime;
|
|
114
|
+
if (this.config.verbose) {
|
|
115
|
+
console.log(`[claude] process exited code=${code} elapsed=${elapsed}ms output=${stdout.length} chars`);
|
|
116
|
+
}
|
|
111
117
|
if (code !== 0) {
|
|
112
118
|
reject(new Error(`Claude CLI failed: ${stderr}`));
|
|
113
119
|
} else {
|
|
@@ -128,18 +134,30 @@ var ClaudeCLI = class {
|
|
|
128
134
|
async *stream(messages, model = "sonnet", systemPrompt) {
|
|
129
135
|
const prompt = this.buildPrompt(messages, systemPrompt);
|
|
130
136
|
const normalizedModel = this.normalizeModel(model);
|
|
131
|
-
const
|
|
137
|
+
const startTime = Date.now();
|
|
138
|
+
const args = ["-p", "--verbose", "--model", normalizedModel, "--output-format", "stream-json"];
|
|
132
139
|
if (this.config.verbose) {
|
|
133
|
-
console.log(`
|
|
140
|
+
console.log(`[claude] spawning stream: ${this.config.cliPath} ${args.join(" ")}`);
|
|
141
|
+
console.log(`[claude] prompt length: ${prompt.length} chars`);
|
|
134
142
|
}
|
|
135
143
|
const proc = spawn(this.config.cliPath, args, {
|
|
136
144
|
stdio: ["pipe", "pipe", "pipe"]
|
|
137
145
|
});
|
|
146
|
+
proc.on("close", (code) => {
|
|
147
|
+
if (this.config.verbose) {
|
|
148
|
+
const elapsed = Date.now() - startTime;
|
|
149
|
+
console.log(`[claude] stream process exited code=${code} elapsed=${elapsed}ms`);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
138
152
|
proc.stdin.write(prompt);
|
|
139
153
|
proc.stdin.end();
|
|
140
154
|
let buffer = "";
|
|
141
155
|
for await (const chunk of proc.stdout) {
|
|
142
|
-
|
|
156
|
+
const chunkStr = chunk.toString();
|
|
157
|
+
buffer += chunkStr;
|
|
158
|
+
if (this.config.verbose) {
|
|
159
|
+
console.log(`[claude] stdout chunk: ${chunkStr.length} bytes`);
|
|
160
|
+
}
|
|
143
161
|
while (buffer.includes("\n")) {
|
|
144
162
|
const [line, rest] = buffer.split("\n", 2);
|
|
145
163
|
buffer = rest ?? "";
|
|
@@ -147,12 +165,21 @@ var ClaudeCLI = class {
|
|
|
147
165
|
if (!trimmed) continue;
|
|
148
166
|
try {
|
|
149
167
|
const data = JSON.parse(trimmed);
|
|
150
|
-
if (
|
|
168
|
+
if (this.config.verbose) {
|
|
169
|
+
console.log(`[claude] parsed type=${data.type}`);
|
|
170
|
+
}
|
|
171
|
+
if (data.type === "assistant" && data.message?.content) {
|
|
172
|
+
for (const block of data.message.content) {
|
|
173
|
+
if (block.type === "text" && block.text) {
|
|
174
|
+
yield block.text;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} else if (data.type === "content_block_delta" && data.delta?.text) {
|
|
178
|
+
yield data.delta.text;
|
|
179
|
+
} else if (data.content) {
|
|
151
180
|
yield data.content;
|
|
152
181
|
} else if (data.text) {
|
|
153
182
|
yield data.text;
|
|
154
|
-
} else if (data.delta?.text) {
|
|
155
|
-
yield data.delta.text;
|
|
156
183
|
}
|
|
157
184
|
} catch {
|
|
158
185
|
if (!trimmed.startsWith("{")) {
|
|
@@ -165,6 +192,13 @@ var ClaudeCLI = class {
|
|
|
165
192
|
};
|
|
166
193
|
|
|
167
194
|
// src/server.ts
|
|
195
|
+
var _verbose = false;
|
|
196
|
+
function log(...args) {
|
|
197
|
+
if (_verbose) {
|
|
198
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
199
|
+
console.log(`[${timestamp}]`, ...args);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
168
202
|
var _cli = null;
|
|
169
203
|
function sendJson(res, data, status = 200) {
|
|
170
204
|
const body = JSON.stringify(data);
|
|
@@ -235,6 +269,9 @@ async function handleRequest(req, res) {
|
|
|
235
269
|
}
|
|
236
270
|
const chatId = generateId("chatcmpl");
|
|
237
271
|
const created = Math.floor(Date.now() / 1e3);
|
|
272
|
+
const msgCount = request.messages?.length ?? 0;
|
|
273
|
+
const lastMsg = request.messages?.[msgCount - 1]?.content?.slice(0, 30) ?? "";
|
|
274
|
+
log(`\u2192 POST /v1/chat/completions model=${request.model} stream=${!!request.stream} msgs=${msgCount} "${lastMsg}${lastMsg.length >= 30 ? "..." : ""}"`);
|
|
238
275
|
if (request.stream) {
|
|
239
276
|
res.writeHead(200, {
|
|
240
277
|
"Content-Type": "text/event-stream",
|
|
@@ -251,8 +288,10 @@ async function handleRequest(req, res) {
|
|
|
251
288
|
choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }]
|
|
252
289
|
};
|
|
253
290
|
sendSSEChunk(res, JSON.stringify(initial));
|
|
291
|
+
let totalLen = 0;
|
|
254
292
|
try {
|
|
255
293
|
for await (const text of _cli.stream(request.messages, request.model)) {
|
|
294
|
+
totalLen += text.length;
|
|
256
295
|
const chunk = {
|
|
257
296
|
id: chatId,
|
|
258
297
|
object: "chat.completion.chunk",
|
|
@@ -262,7 +301,9 @@ async function handleRequest(req, res) {
|
|
|
262
301
|
};
|
|
263
302
|
sendSSEChunk(res, JSON.stringify(chunk));
|
|
264
303
|
}
|
|
304
|
+
log(`\u2190 stream complete, total length=${totalLen}`);
|
|
265
305
|
} catch (err) {
|
|
306
|
+
log(`\u2190 stream error: ${err}`);
|
|
266
307
|
sendSSEChunk(res, JSON.stringify({ error: { message: String(err), type: "server_error" } }));
|
|
267
308
|
}
|
|
268
309
|
const final = {
|
|
@@ -279,6 +320,7 @@ async function handleRequest(req, res) {
|
|
|
279
320
|
}
|
|
280
321
|
try {
|
|
281
322
|
const content = await _cli.complete(request.messages, request.model);
|
|
323
|
+
log(`\u2190 response complete, length=${content.length}`);
|
|
282
324
|
const response = {
|
|
283
325
|
id: chatId,
|
|
284
326
|
object: "chat.completion",
|
|
@@ -289,6 +331,7 @@ async function handleRequest(req, res) {
|
|
|
289
331
|
};
|
|
290
332
|
sendJson(res, response);
|
|
291
333
|
} catch (err) {
|
|
334
|
+
log(`\u2190 error: ${err}`);
|
|
292
335
|
sendError(res, String(err));
|
|
293
336
|
}
|
|
294
337
|
return;
|
|
@@ -296,8 +339,9 @@ async function handleRequest(req, res) {
|
|
|
296
339
|
sendError(res, "Not found", 404);
|
|
297
340
|
}
|
|
298
341
|
function createApp(config) {
|
|
342
|
+
_verbose = config?.verbose ?? process.env.CLAUDE_CODE_RELAY_VERBOSE === "1";
|
|
299
343
|
try {
|
|
300
|
-
_cli = new ClaudeCLI(config?.cli);
|
|
344
|
+
_cli = new ClaudeCLI({ ...config?.cli, verbose: _verbose });
|
|
301
345
|
} catch (err) {
|
|
302
346
|
console.error(`Failed to initialize Claude CLI: ${err}`);
|
|
303
347
|
_cli = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-relay",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Local proxy that exposes Claude CLI as an OpenAI-compatible API server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -60,5 +60,8 @@
|
|
|
60
60
|
"esbuild": "^0.27.2",
|
|
61
61
|
"typescript": "^5.9.3",
|
|
62
62
|
"vitest": "^4.0.16"
|
|
63
|
+
},
|
|
64
|
+
"optionalDependencies": {
|
|
65
|
+
"@rollup/rollup-linux-x64-gnu": "^4.55.1"
|
|
63
66
|
}
|
|
64
67
|
}
|