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.
Files changed (44) hide show
  1. package/README.md +199 -0
  2. package/dist/cjs/cli-wrapper.js +186 -0
  3. package/dist/cjs/cli.js +400 -0
  4. package/dist/cjs/index.js +351 -0
  5. package/dist/cjs/server.js +347 -0
  6. package/dist/cjs/types.js +18 -0
  7. package/dist/cjs/utils.js +53 -0
  8. package/dist/esm/cli-wrapper.d.ts.map +1 -0
  9. package/dist/esm/cli-wrapper.js +161 -0
  10. package/dist/esm/cli.d.ts.map +1 -0
  11. package/dist/esm/cli.js +399 -0
  12. package/dist/esm/index.d.ts.map +1 -0
  13. package/dist/esm/index.js +322 -0
  14. package/dist/esm/server.d.ts.map +1 -0
  15. package/dist/esm/server.js +321 -0
  16. package/dist/esm/types.d.ts.map +1 -0
  17. package/dist/esm/types.js +0 -0
  18. package/dist/esm/utils.d.ts.map +1 -0
  19. package/dist/esm/utils.js +27 -0
  20. package/package.json +17 -8
  21. package/dist/cli-wrapper.d.ts.map +0 -1
  22. package/dist/cli-wrapper.js +0 -149
  23. package/dist/cli-wrapper.js.map +0 -1
  24. package/dist/cli.d.ts.map +0 -1
  25. package/dist/cli.js +0 -87
  26. package/dist/cli.js.map +0 -1
  27. package/dist/index.d.ts.map +0 -1
  28. package/dist/index.js +0 -6
  29. package/dist/index.js.map +0 -1
  30. package/dist/server.d.ts.map +0 -1
  31. package/dist/server.js +0 -168
  32. package/dist/server.js.map +0 -1
  33. package/dist/types.d.ts.map +0 -1
  34. package/dist/types.js +0 -5
  35. package/dist/types.js.map +0 -1
  36. package/dist/utils.d.ts.map +0 -1
  37. package/dist/utils.js +0 -34
  38. package/dist/utils.js.map +0 -1
  39. /package/dist/{cli-wrapper.d.ts → esm/cli-wrapper.d.ts} +0 -0
  40. /package/dist/{cli.d.ts → esm/cli.d.ts} +0 -0
  41. /package/dist/{index.d.ts → esm/index.d.ts} +0 -0
  42. /package/dist/{server.d.ts → esm/server.d.ts} +0 -0
  43. /package/dist/{types.d.ts → esm/types.d.ts} +0 -0
  44. /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
+ });