claude-code-relay 0.0.1 → 0.0.7
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 +186 -0
- package/dist/cjs/cli.js +400 -0
- package/dist/cjs/index.js +351 -0
- package/dist/cjs/server.js +347 -0
- package/dist/cjs/types.js +18 -0
- package/dist/cjs/utils.js +53 -0
- package/dist/esm/cli-wrapper.d.ts.map +1 -0
- package/dist/esm/cli-wrapper.js +161 -0
- package/dist/esm/cli.d.ts.map +1 -0
- package/dist/esm/cli.js +399 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +322 -0
- package/dist/esm/server.d.ts.map +1 -0
- package/dist/esm/server.js +321 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +0 -0
- package/dist/esm/utils.d.ts.map +1 -0
- package/dist/esm/utils.js +27 -0
- package/package.json +17 -8
- package/dist/cli-wrapper.d.ts.map +0 -1
- package/dist/cli-wrapper.js +0 -149
- package/dist/cli-wrapper.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -87
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -6
- package/dist/index.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js +0 -168
- package/dist/server.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -34
- package/dist/utils.js.map +0 -1
- /package/dist/{cli-wrapper.d.ts → esm/cli-wrapper.d.ts} +0 -0
- /package/dist/{cli.d.ts → esm/cli.d.ts} +0 -0
- /package/dist/{index.d.ts → esm/index.d.ts} +0 -0
- /package/dist/{server.d.ts → esm/server.d.ts} +0 -0
- /package/dist/{types.d.ts → esm/types.d.ts} +0 -0
- /package/dist/{utils.d.ts → esm/utils.d.ts} +0 -0
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.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/cli-wrapper.ts
|
|
21
|
+
var cli_wrapper_exports = {};
|
|
22
|
+
__export(cli_wrapper_exports, {
|
|
23
|
+
ClaudeCLI: () => ClaudeCLI
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(cli_wrapper_exports);
|
|
26
|
+
var import_node_child_process2 = require("node:child_process");
|
|
27
|
+
|
|
28
|
+
// src/utils.ts
|
|
29
|
+
var import_node_child_process = require("node:child_process");
|
|
30
|
+
var import_node_fs = require("node:fs");
|
|
31
|
+
var import_node_os = require("node:os");
|
|
32
|
+
function which(command) {
|
|
33
|
+
if (command.startsWith("/") || command.startsWith("~")) {
|
|
34
|
+
return (0, import_node_fs.existsSync)(command) ? command : null;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const cmd = (0, import_node_os.platform)() === "win32" ? "where" : "which";
|
|
38
|
+
const result = (0, import_node_child_process.execSync)(`${cmd} ${command}`, {
|
|
39
|
+
encoding: "utf-8",
|
|
40
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
41
|
+
});
|
|
42
|
+
return result.trim().split("\n")[0] ?? null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/cli-wrapper.ts
|
|
49
|
+
var MODEL_MAP = {
|
|
50
|
+
sonnet: "sonnet",
|
|
51
|
+
opus: "opus",
|
|
52
|
+
haiku: "haiku",
|
|
53
|
+
"claude-3-sonnet": "sonnet",
|
|
54
|
+
"claude-3-opus": "opus",
|
|
55
|
+
"claude-3-haiku": "haiku",
|
|
56
|
+
"claude-sonnet-4": "sonnet",
|
|
57
|
+
"claude-opus-4": "opus"
|
|
58
|
+
};
|
|
59
|
+
var ClaudeCLI = class {
|
|
60
|
+
config;
|
|
61
|
+
constructor(config) {
|
|
62
|
+
this.config = {
|
|
63
|
+
cliPath: config?.cliPath ?? process.env.CLAUDE_CLI_PATH ?? "claude",
|
|
64
|
+
timeout: config?.timeout ?? parseInt(process.env.CLAUDE_CODE_RELAY_TIMEOUT ?? "300", 10),
|
|
65
|
+
verbose: config?.verbose ?? process.env.CLAUDE_CODE_RELAY_VERBOSE === "1"
|
|
66
|
+
};
|
|
67
|
+
this.validateCLI();
|
|
68
|
+
}
|
|
69
|
+
validateCLI() {
|
|
70
|
+
const cliPath = which(this.config.cliPath);
|
|
71
|
+
if (!cliPath) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Claude CLI not found at '${this.config.cliPath}'. Please install it or set CLAUDE_CLI_PATH.`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (this.config.verbose) {
|
|
77
|
+
console.log(`Using Claude CLI at: ${cliPath}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
normalizeModel(model) {
|
|
81
|
+
return MODEL_MAP[model.toLowerCase()] ?? "sonnet";
|
|
82
|
+
}
|
|
83
|
+
buildPrompt(messages, systemPrompt) {
|
|
84
|
+
const parts = [];
|
|
85
|
+
for (const msg of messages) {
|
|
86
|
+
if (msg.role === "system") {
|
|
87
|
+
systemPrompt = msg.content;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (systemPrompt) {
|
|
92
|
+
parts.push(`System: ${systemPrompt}
|
|
93
|
+
`);
|
|
94
|
+
}
|
|
95
|
+
for (const msg of messages) {
|
|
96
|
+
if (msg.role === "system") continue;
|
|
97
|
+
if (msg.role === "user") {
|
|
98
|
+
parts.push(`Human: ${msg.content}
|
|
99
|
+
`);
|
|
100
|
+
} else if (msg.role === "assistant") {
|
|
101
|
+
parts.push(`Assistant: ${msg.content}
|
|
102
|
+
`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
parts.push("Assistant:");
|
|
106
|
+
return parts.join("\n");
|
|
107
|
+
}
|
|
108
|
+
async complete(messages, model = "sonnet", systemPrompt) {
|
|
109
|
+
const prompt = this.buildPrompt(messages, systemPrompt);
|
|
110
|
+
const normalizedModel = this.normalizeModel(model);
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const args = ["-p", "--model", normalizedModel, "--output-format", "text"];
|
|
113
|
+
if (this.config.verbose) {
|
|
114
|
+
console.log(`Running: ${this.config.cliPath} ${args.join(" ")}`);
|
|
115
|
+
}
|
|
116
|
+
const proc = (0, import_node_child_process2.spawn)(this.config.cliPath, args, {
|
|
117
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
118
|
+
});
|
|
119
|
+
let stdout = "";
|
|
120
|
+
let stderr = "";
|
|
121
|
+
proc.stdout.on("data", (data) => {
|
|
122
|
+
stdout += data.toString();
|
|
123
|
+
});
|
|
124
|
+
proc.stderr.on("data", (data) => {
|
|
125
|
+
stderr += data.toString();
|
|
126
|
+
});
|
|
127
|
+
proc.on("close", (code) => {
|
|
128
|
+
if (code !== 0) {
|
|
129
|
+
reject(new Error(`Claude CLI failed: ${stderr}`));
|
|
130
|
+
} else {
|
|
131
|
+
resolve(stdout.trim());
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
proc.on("error", (err) => {
|
|
135
|
+
reject(err);
|
|
136
|
+
});
|
|
137
|
+
proc.stdin.write(prompt);
|
|
138
|
+
proc.stdin.end();
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
proc.kill();
|
|
141
|
+
reject(new Error(`Claude CLI timeout after ${this.config.timeout}s`));
|
|
142
|
+
}, this.config.timeout * 1e3);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
async *stream(messages, model = "sonnet", systemPrompt) {
|
|
146
|
+
const prompt = this.buildPrompt(messages, systemPrompt);
|
|
147
|
+
const normalizedModel = this.normalizeModel(model);
|
|
148
|
+
const args = ["-p", "--model", normalizedModel, "--output-format", "stream-json"];
|
|
149
|
+
if (this.config.verbose) {
|
|
150
|
+
console.log(`Running: ${this.config.cliPath} ${args.join(" ")}`);
|
|
151
|
+
}
|
|
152
|
+
const proc = (0, import_node_child_process2.spawn)(this.config.cliPath, args, {
|
|
153
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
154
|
+
});
|
|
155
|
+
proc.stdin.write(prompt);
|
|
156
|
+
proc.stdin.end();
|
|
157
|
+
let buffer = "";
|
|
158
|
+
for await (const chunk of proc.stdout) {
|
|
159
|
+
buffer += chunk.toString();
|
|
160
|
+
while (buffer.includes("\n")) {
|
|
161
|
+
const [line, rest] = buffer.split("\n", 2);
|
|
162
|
+
buffer = rest ?? "";
|
|
163
|
+
const trimmed = line.trim();
|
|
164
|
+
if (!trimmed) continue;
|
|
165
|
+
try {
|
|
166
|
+
const data = JSON.parse(trimmed);
|
|
167
|
+
if (data.content) {
|
|
168
|
+
yield data.content;
|
|
169
|
+
} else if (data.text) {
|
|
170
|
+
yield data.text;
|
|
171
|
+
} else if (data.delta?.text) {
|
|
172
|
+
yield data.delta.text;
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
if (!trimmed.startsWith("{")) {
|
|
176
|
+
yield trimmed;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
184
|
+
0 && (module.exports = {
|
|
185
|
+
ClaudeCLI
|
|
186
|
+
});
|