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 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.
@@ -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(`Running: ${this.config.cliPath} ${args.join(" ")}`);
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 args = ["-p", "--model", normalizedModel, "--output-format", "stream-json"];
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(`Running: ${this.config.cliPath} ${args.join(" ")}`);
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
- buffer += chunk.toString();
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 (data.content) {
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(`Running: ${this.config.cliPath} ${args.join(" ")}`);
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 args = ["-p", "--model", normalizedModel, "--output-format", "stream-json"];
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(`Running: ${this.config.cliPath} ${args.join(" ")}`);
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
- buffer += chunk.toString();
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 (data.content) {
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(`Running: ${this.config.cliPath} ${args.join(" ")}`);
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 args = ["-p", "--model", normalizedModel, "--output-format", "stream-json"];
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(`Running: ${this.config.cliPath} ${args.join(" ")}`);
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
- buffer += chunk.toString();
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 (data.content) {
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;
@@ -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(`Running: ${this.config.cliPath} ${args.join(" ")}`);
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 args = ["-p", "--model", normalizedModel, "--output-format", "stream-json"];
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(`Running: ${this.config.cliPath} ${args.join(" ")}`);
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
- buffer += chunk.toString();
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 (data.content) {
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;IAkDX,MAAM,CACX,QAAQ,EAAE,WAAW,EAAE,EACvB,KAAK,SAAW,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC;CAiDzC"}
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"}
@@ -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(`Running: ${this.config.cliPath} ${args.join(" ")}`);
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 args = ["-p", "--model", normalizedModel, "--output-format", "stream-json"];
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(`Running: ${this.config.cliPath} ${args.join(" ")}`);
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
- buffer += chunk.toString();
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 (data.content) {
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(`Running: ${this.config.cliPath} ${args.join(" ")}`);
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 args = ["-p", "--model", normalizedModel, "--output-format", "stream-json"];
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(`Running: ${this.config.cliPath} ${args.join(" ")}`);
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
- buffer += chunk.toString();
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 (data.content) {
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(`Running: ${this.config.cliPath} ${args.join(" ")}`);
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 args = ["-p", "--model", normalizedModel, "--output-format", "stream-json"];
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(`Running: ${this.config.cliPath} ${args.join(" ")}`);
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
- buffer += chunk.toString();
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 (data.content) {
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;
@@ -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;
@@ -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;CAC1B;AA6JD,wBAAgB,SAAS,CAAC,MAAM,CAAC,EAAE,SAAS,6EAc3C;AAED,wBAAgB,SAAS,CAAC,IAAI,SAAc,EAAE,IAAI,SAAQ,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,IAAI,CAKpF"}
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"}
@@ -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(`Running: ${this.config.cliPath} ${args.join(" ")}`);
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 args = ["-p", "--model", normalizedModel, "--output-format", "stream-json"];
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(`Running: ${this.config.cliPath} ${args.join(" ")}`);
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
- buffer += chunk.toString();
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 (data.content) {
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.4",
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
  }