opencode-claude-max-proxy 1.0.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +272 -113
- package/bin/claude-proxy-supervisor.sh +62 -0
- package/bin/claude-proxy.ts +26 -1
- package/package.json +9 -6
- package/src/logger.ts +68 -6
- package/src/proxy/agentDefs.ts +102 -0
- package/src/proxy/agentMatch.ts +93 -0
- package/src/proxy/passthroughTools.ts +108 -0
- package/src/proxy/server.ts +947 -118
- package/src/proxy/types.ts +3 -1
package/README.md
CHANGED
|
@@ -4,199 +4,358 @@
|
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[](https://github.com/rynfar/opencode-claude-max-proxy/stargazers)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
A transparent proxy that lets you use your **Claude Max subscription** with [OpenCode](https://opencode.ai) — with full multi-model agent delegation.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## The Idea
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
OpenCode speaks the Anthropic API. Claude Max gives you unlimited Claude through the [Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk). This proxy bridges the two — but the interesting part isn't the bridging, it's **how tool execution and agent delegation work**.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Most proxy approaches try to handle everything internally: the SDK runs tools, manages subagents, and streams back a finished result. The problem? The SDK only knows about Claude models. If you're using [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) with agents routed to GPT-5.4, Gemini, or other providers, the SDK can't reach them. Your carefully configured multi-model agent setup gets flattened to "everything runs on Claude."
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
1. Use Claude's official apps only (limited to their UI)
|
|
17
|
-
2. Pay again for API access on top of your Max subscription
|
|
18
|
-
3. **Use this proxy**
|
|
15
|
+
This proxy takes a different approach: **passthrough delegation**. Instead of handling tools internally, the proxy intercepts every tool call *before the SDK executes it*, stops the turn, and forwards the raw `tool_use` back to OpenCode. OpenCode handles execution — including routing `Task` calls through its own agent system with full model routing. The result comes back as a standard `tool_result`, the proxy resumes the SDK session, and Claude continues.
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
This proxy bridges the gap using Anthropic's own tools:
|
|
17
|
+
The effect: Claude thinks it's calling tools normally. OpenCode thinks it's talking to the Anthropic API. But behind the scenes, your oracle agent runs on GPT-5.4, your explore agent runs on Gemini, and the main session runs on Claude Max — exactly as configured.
|
|
23
18
|
|
|
24
19
|
```
|
|
25
|
-
|
|
20
|
+
┌──────────┐ ┌───────────────┐ ┌──────────────┐
|
|
21
|
+
│ │ 1. Request │ │ SDK Auth │ │
|
|
22
|
+
│ OpenCode │ ─────────────► │ Proxy │ ───────────► │ Claude Max │
|
|
23
|
+
│ │ │ (localhost) │ │ │
|
|
24
|
+
│ │ 4. tool_use │ │ 2. Generate │ │
|
|
25
|
+
│ │ ◄───────────── │ PreToolUse │ ◄─────────── │ Response │
|
|
26
|
+
│ │ │ hook blocks │ │ │
|
|
27
|
+
│ │ 5. Execute │ │ └──────────────┘
|
|
28
|
+
│ │ ─── Task ───► │ │
|
|
29
|
+
│ │ (routes to │ │ ┌──────────────┐
|
|
30
|
+
│ │ GPT-5.4, │ 6. Resume │ │ oh-my- │
|
|
31
|
+
│ │ Gemini,etc) │ ◄──────────── │ │ opencode │
|
|
32
|
+
│ │ │ tool_result │ │ agents │
|
|
33
|
+
│ │ 7. Final │ │ │ │
|
|
34
|
+
│ │ ◄───────────── │ │ │ oracle: │
|
|
35
|
+
│ │ response │ │ │ GPT-5.4 │
|
|
36
|
+
└──────────┘ └───────────────┘ │ explore: │
|
|
37
|
+
│ Gemini │
|
|
38
|
+
│ librarian: │
|
|
39
|
+
│ Sonnet │
|
|
40
|
+
└──────────────┘
|
|
26
41
|
```
|
|
27
42
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
**Your Max subscription. Anthropic's official SDK. Zero additional cost.**
|
|
31
|
-
|
|
32
|
-
## Is This Allowed?
|
|
33
|
-
|
|
34
|
-
**Yes.** Here's why:
|
|
43
|
+
## How Passthrough Works
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
|---------|---------|
|
|
38
|
-
| "Bypassing restrictions" | No. We use Anthropic's public SDK exactly as documented |
|
|
39
|
-
| "Violating TOS" | No. The SDK is designed for programmatic Claude access |
|
|
40
|
-
| "Unauthorized access" | No. You authenticate with `claude login` using your own account |
|
|
41
|
-
| "Reverse engineering" | No. We call `query()` from their npm package, that's it |
|
|
45
|
+
The key insight is the Claude Agent SDK's `PreToolUse` hook — an officially supported callback that fires before any tool executes. Combined with `maxTurns: 1`, it gives us precise control over the execution boundary:
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
1. **Claude generates a response** with `tool_use` blocks (Read a file, delegate to an agent, run a command)
|
|
48
|
+
2. **The PreToolUse hook fires** for each tool call — we capture the tool name, input, and ID, then return `decision: "block"` to prevent SDK-internal execution
|
|
49
|
+
3. **The SDK stops** (blocked tool + maxTurns:1 = turn complete) and we have the full tool_use payload
|
|
50
|
+
4. **The proxy returns it to OpenCode** as a standard Anthropic API response with `stop_reason: "tool_use"`
|
|
51
|
+
5. **OpenCode handles everything** — file reads, shell commands, and crucially, `Task` delegation through its own agent system with full model routing
|
|
52
|
+
6. **OpenCode sends `tool_result` back**, the proxy resumes the SDK session, and Claude continues
|
|
44
53
|
|
|
45
|
-
|
|
54
|
+
No monkey-patching. No forked SDKs. No fragile stream rewriting. Just a hook and a turn limit.
|
|
46
55
|
|
|
47
|
-
##
|
|
56
|
+
## Quick Start
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|---------|-------------|
|
|
51
|
-
| **Zero API costs** | Uses your Claude Max subscription, not per-token billing |
|
|
52
|
-
| **Full compatibility** | Works with any Anthropic model in OpenCode |
|
|
53
|
-
| **Streaming support** | Real-time SSE streaming just like the real API |
|
|
54
|
-
| **Auto-start** | Optional launchd service for macOS |
|
|
55
|
-
| **Simple setup** | Two commands to get running |
|
|
58
|
+
### Prerequisites
|
|
56
59
|
|
|
57
|
-
|
|
60
|
+
1. **Claude Max subscription** — [Subscribe here](https://claude.ai/settings/subscription)
|
|
61
|
+
2. **Claude CLI** authenticated: `npm install -g @anthropic-ai/claude-code && claude login`
|
|
58
62
|
|
|
59
|
-
|
|
63
|
+
### Option A: npm Install
|
|
60
64
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
npm install -g @anthropic-ai/claude-code
|
|
64
|
-
claude login
|
|
65
|
-
```
|
|
65
|
+
```bash
|
|
66
|
+
npm install -g opencode-claude-max-proxy
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
```
|
|
68
|
+
# Start in passthrough mode (recommended)
|
|
69
|
+
CLAUDE_PROXY_PASSTHROUGH=1 claude-max-proxy
|
|
70
|
+
```
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
### Option B: From Source
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
75
|
git clone https://github.com/rynfar/opencode-claude-max-proxy
|
|
76
76
|
cd opencode-claude-max-proxy
|
|
77
77
|
bun install
|
|
78
|
+
|
|
79
|
+
# Start in passthrough mode (recommended)
|
|
80
|
+
CLAUDE_PROXY_PASSTHROUGH=1 bun run proxy
|
|
78
81
|
```
|
|
79
82
|
|
|
80
|
-
|
|
83
|
+
> **Note:** Running from source requires [Bun](https://bun.sh): `curl -fsSL https://bun.sh/install | bash`
|
|
81
84
|
|
|
82
|
-
###
|
|
85
|
+
### Option C: Docker
|
|
83
86
|
|
|
84
87
|
```bash
|
|
85
|
-
|
|
88
|
+
git clone https://github.com/rynfar/opencode-claude-max-proxy
|
|
89
|
+
cd opencode-claude-max-proxy
|
|
90
|
+
|
|
91
|
+
# Start the container
|
|
92
|
+
docker compose up -d
|
|
93
|
+
|
|
94
|
+
# Login to Claude inside the container (one-time)
|
|
95
|
+
docker compose exec proxy claude login
|
|
96
|
+
|
|
97
|
+
# Verify
|
|
98
|
+
curl http://127.0.0.1:3456/health
|
|
86
99
|
```
|
|
87
100
|
|
|
88
|
-
|
|
101
|
+
> **Note:** On macOS, `claude login` must be run inside the container (keychain credentials can't be mounted). On Linux, volume-mounting `~/.claude` may work without re-login.
|
|
102
|
+
|
|
103
|
+
### Connect OpenCode
|
|
89
104
|
|
|
90
105
|
```bash
|
|
91
106
|
ANTHROPIC_API_KEY=dummy ANTHROPIC_BASE_URL=http://127.0.0.1:3456 opencode
|
|
92
107
|
```
|
|
93
108
|
|
|
94
|
-
|
|
109
|
+
The `ANTHROPIC_API_KEY` can be any non-empty string — the proxy doesn't use it. Authentication is handled by your `claude login` session.
|
|
95
110
|
|
|
96
|
-
###
|
|
111
|
+
### Shell Alias
|
|
97
112
|
|
|
98
113
|
```bash
|
|
99
|
-
|
|
114
|
+
# Add to ~/.zshrc or ~/.bashrc
|
|
115
|
+
alias oc='ANTHROPIC_API_KEY=dummy ANTHROPIC_BASE_URL=http://127.0.0.1:3456 opencode'
|
|
100
116
|
```
|
|
101
117
|
|
|
102
|
-
##
|
|
118
|
+
## Modes
|
|
103
119
|
|
|
104
|
-
|
|
120
|
+
### Passthrough Mode (recommended)
|
|
105
121
|
|
|
106
122
|
```bash
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
110
|
-
<plist version="1.0">
|
|
111
|
-
<dict>
|
|
112
|
-
<key>Label</key>
|
|
113
|
-
<string>com.claude-max-proxy</string>
|
|
114
|
-
<key>ProgramArguments</key>
|
|
115
|
-
<array>
|
|
116
|
-
<string>$(which bun)</string>
|
|
117
|
-
<string>run</string>
|
|
118
|
-
<string>proxy</string>
|
|
119
|
-
</array>
|
|
120
|
-
<key>WorkingDirectory</key>
|
|
121
|
-
<string>$(pwd)</string>
|
|
122
|
-
<key>RunAtLoad</key>
|
|
123
|
-
<true/>
|
|
124
|
-
<key>KeepAlive</key>
|
|
125
|
-
<true/>
|
|
126
|
-
</dict>
|
|
127
|
-
</plist>
|
|
128
|
-
EOF
|
|
123
|
+
CLAUDE_PROXY_PASSTHROUGH=1 bun run proxy
|
|
124
|
+
```
|
|
129
125
|
|
|
130
|
-
|
|
126
|
+
All tool execution is forwarded to OpenCode. This enables:
|
|
127
|
+
|
|
128
|
+
- **Multi-model agent delegation** — oh-my-opencode routes each agent to its configured model
|
|
129
|
+
- **Full agent system prompts** — not abbreviated descriptions, the real prompts
|
|
130
|
+
- **OpenCode manages everything** — tools, agents, permissions, lifecycle
|
|
131
|
+
|
|
132
|
+
### Internal Mode (default)
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
bun run proxy
|
|
131
136
|
```
|
|
132
137
|
|
|
133
|
-
|
|
138
|
+
Tools execute inside the proxy via MCP. Subagents run on Claude via the SDK's native agent system. Simpler setup, but all agents use Claude regardless of oh-my-opencode config.
|
|
139
|
+
|
|
140
|
+
| | Passthrough | Internal |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| Tool execution | OpenCode | Proxy (MCP) |
|
|
143
|
+
| Agent delegation | OpenCode → multi-model | SDK → Claude only |
|
|
144
|
+
| oh-my-opencode models | ✅ Respected | ❌ All Claude |
|
|
145
|
+
| Agent system prompts | ✅ Full | ⚠️ Description only |
|
|
146
|
+
| Setup complexity | Same | Same |
|
|
147
|
+
|
|
148
|
+
## Works With Any Agent Framework
|
|
149
|
+
|
|
150
|
+
The proxy extracts agent definitions from the `Task` tool description that OpenCode sends in each request. This means it works automatically with:
|
|
151
|
+
|
|
152
|
+
- **Native OpenCode** — `build` and `plan` agents
|
|
153
|
+
- **oh-my-opencode** — `oracle`, `explore`, `librarian`, `sisyphus-junior`, `metis`, `momus`, etc.
|
|
154
|
+
- **Custom agents** — anything you define in `opencode.json`
|
|
155
|
+
|
|
156
|
+
In internal mode, a `PreToolUse` hook fuzzy-matches agent names as a safety net (e.g., `general-purpose` → `general`, `Explore` → `explore`, `code-reviewer` → `oracle`). In passthrough mode, OpenCode handles agent names directly.
|
|
157
|
+
|
|
158
|
+
## Session Resume
|
|
159
|
+
|
|
160
|
+
The proxy tracks SDK session IDs and resumes conversations on follow-up requests:
|
|
161
|
+
|
|
162
|
+
- **Faster responses** — no re-processing of conversation history
|
|
163
|
+
- **Better context** — the SDK remembers tool results from previous turns
|
|
164
|
+
|
|
165
|
+
Session tracking works two ways:
|
|
166
|
+
|
|
167
|
+
1. **Header-based** (recommended) — Add the included OpenCode plugin:
|
|
168
|
+
```json
|
|
169
|
+
{ "plugin": ["./path/to/opencode-claude-max-proxy/src/plugin/claude-max-headers.ts"] }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
2. **Fingerprint-based** (automatic fallback) — hashes the first user message to identify returning conversations
|
|
173
|
+
|
|
174
|
+
Sessions are cached for 24 hours.
|
|
175
|
+
|
|
176
|
+
## Configuration
|
|
177
|
+
|
|
178
|
+
| Variable | Default | Description |
|
|
179
|
+
|----------|---------|-------------|
|
|
180
|
+
| `CLAUDE_PROXY_PASSTHROUGH` | (unset) | Enable passthrough mode — forward all tools to OpenCode |
|
|
181
|
+
| `CLAUDE_PROXY_PORT` | 3456 | Proxy server port |
|
|
182
|
+
| `CLAUDE_PROXY_HOST` | 127.0.0.1 | Proxy server host |
|
|
183
|
+
| `CLAUDE_PROXY_WORKDIR` | (cwd) | Working directory for Claude and tools |
|
|
184
|
+
| `CLAUDE_PROXY_MAX_CONCURRENT` | 1 | Max concurrent SDK sessions (increase with caution) |
|
|
185
|
+
| `CLAUDE_PROXY_IDLE_TIMEOUT_SECONDS` | 120 | Connection idle timeout |
|
|
186
|
+
|
|
187
|
+
## Concurrency
|
|
188
|
+
|
|
189
|
+
The proxy supports multiple simultaneous OpenCode instances. Each request spawns its own independent SDK subprocess — run as many terminals as you want. All concurrent responses are delivered correctly.
|
|
190
|
+
|
|
191
|
+
**Use the auto-restart supervisor** (recommended):
|
|
134
192
|
|
|
135
193
|
```bash
|
|
136
|
-
|
|
137
|
-
|
|
194
|
+
CLAUDE_PROXY_PASSTHROUGH=1 bun run proxy
|
|
195
|
+
# or directly:
|
|
196
|
+
CLAUDE_PROXY_PASSTHROUGH=1 ./bin/claude-proxy-supervisor.sh
|
|
138
197
|
```
|
|
139
198
|
|
|
140
|
-
|
|
199
|
+
> **⚠️ Known Issue: Bun SSE Crash ([oven-sh/bun#17947](https://github.com/oven-sh/bun/issues/17947))**
|
|
200
|
+
>
|
|
201
|
+
> The Claude Agent SDK's `cli.js` subprocess is compiled with Bun, which has a known segfault in `structuredCloneForStream` during cleanup of concurrent streaming responses. This affects all runtimes (Bun, Node.js via tsx) because the crash originates in the SDK's child process, not in the proxy itself.
|
|
202
|
+
>
|
|
203
|
+
> **What this means in practice:**
|
|
204
|
+
> - **Sequential requests (1 terminal):** No impact. Never crashes.
|
|
205
|
+
> - **Concurrent requests (2+ terminals):** All responses are delivered correctly. The crash occurs *after* responses complete, during stream cleanup. No work is lost.
|
|
206
|
+
> - **After a crash:** The supervisor restarts the proxy in ~1-3 seconds. If a new request arrives during this window, OpenCode shows "Unable to connect" — just retry.
|
|
207
|
+
>
|
|
208
|
+
> We are monitoring the upstream Bun issue for a fix. Once patched, the supervisor becomes optional.
|
|
141
209
|
|
|
142
210
|
## Model Mapping
|
|
143
211
|
|
|
144
212
|
| OpenCode Model | Claude SDK |
|
|
145
213
|
|----------------|------------|
|
|
146
214
|
| `anthropic/claude-opus-*` | opus |
|
|
147
|
-
| `anthropic/claude-sonnet-*` | sonnet |
|
|
215
|
+
| `anthropic/claude-sonnet-*` | sonnet (default) |
|
|
148
216
|
| `anthropic/claude-haiku-*` | haiku |
|
|
149
217
|
|
|
150
|
-
##
|
|
151
|
-
|
|
152
|
-
| Environment Variable | Default | Description |
|
|
153
|
-
|---------------------|---------|-------------|
|
|
154
|
-
| `CLAUDE_PROXY_PORT` | 3456 | Proxy server port |
|
|
155
|
-
| `CLAUDE_PROXY_HOST` | 127.0.0.1 | Proxy server host |
|
|
218
|
+
## Disclaimer
|
|
156
219
|
|
|
157
|
-
|
|
220
|
+
This project is an **unofficial wrapper** around Anthropic's publicly available [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk). It is not affiliated with, endorsed by, or supported by Anthropic.
|
|
158
221
|
|
|
159
|
-
|
|
160
|
-
2. **Proxy** receives the request and extracts the messages
|
|
161
|
-
3. **Proxy** calls `query()` from the Claude Agent SDK with your prompt
|
|
162
|
-
4. **Claude Agent SDK** authenticates using your Claude CLI login (tied to your Max subscription)
|
|
163
|
-
5. **Claude** processes the request using your subscription
|
|
164
|
-
6. **Proxy** streams the response back in Anthropic SSE format
|
|
165
|
-
7. **OpenCode** receives the response as if it came from the real API
|
|
222
|
+
**Use at your own risk.** The authors make no claims regarding compliance with Anthropic's Terms of Service. It is your responsibility to review and comply with [Anthropic's Terms of Service](https://www.anthropic.com/terms) and any applicable usage policies. Terms may change at any time.
|
|
166
223
|
|
|
167
|
-
|
|
224
|
+
This project calls `query()` from Anthropic's public npm package using your own authenticated account. No API keys are intercepted, no authentication is bypassed, and no proprietary systems are reverse-engineered.
|
|
168
225
|
|
|
169
226
|
## FAQ
|
|
170
227
|
|
|
171
|
-
|
|
228
|
+
<details>
|
|
229
|
+
<summary><strong>Why passthrough mode instead of handling tools internally?</strong></summary>
|
|
230
|
+
|
|
231
|
+
Internal tool execution means the SDK handles everything — but the SDK only speaks Claude. If your agents are configured for GPT-5.4 or Gemini via oh-my-opencode, that routing gets lost. Passthrough mode preserves the full OpenCode agent pipeline, including multi-model routing, full system prompts, and agent lifecycle management.
|
|
232
|
+
</details>
|
|
233
|
+
|
|
234
|
+
<details>
|
|
235
|
+
<summary><strong>Does this work without oh-my-opencode?</strong></summary>
|
|
236
|
+
|
|
237
|
+
Yes. Both modes work with native OpenCode (build + plan agents) and with any custom agents defined in your `opencode.json`. oh-my-opencode just adds more agents and model routing — the proxy handles whatever OpenCode sends.
|
|
238
|
+
</details>
|
|
172
239
|
|
|
173
|
-
|
|
240
|
+
<details>
|
|
241
|
+
<summary><strong>Why do I need ANTHROPIC_API_KEY=dummy?</strong></summary>
|
|
174
242
|
|
|
175
|
-
|
|
243
|
+
OpenCode requires an API key to be set, but the proxy never uses it. Authentication is handled by your `claude login` session through the Agent SDK.
|
|
244
|
+
</details>
|
|
176
245
|
|
|
177
|
-
|
|
246
|
+
<details>
|
|
247
|
+
<summary><strong>What about rate limits?</strong></summary>
|
|
178
248
|
|
|
179
|
-
|
|
249
|
+
Your Claude Max subscription has its own usage limits. The proxy doesn't add any additional limits. Concurrent requests are supported.
|
|
250
|
+
</details>
|
|
180
251
|
|
|
181
|
-
|
|
252
|
+
<details>
|
|
253
|
+
<summary><strong>Is my data sent anywhere else?</strong></summary>
|
|
182
254
|
|
|
183
|
-
|
|
255
|
+
No. The proxy runs locally. Requests go directly to Claude through the official SDK. In passthrough mode, tool execution happens in OpenCode on your machine.
|
|
256
|
+
</details>
|
|
184
257
|
|
|
185
|
-
|
|
258
|
+
<details>
|
|
259
|
+
<summary><strong>Why does internal mode use MCP tools?</strong></summary>
|
|
260
|
+
|
|
261
|
+
The Claude Agent SDK uses different parameter names for tools than OpenCode (e.g., `file_path` vs `filePath`). Internal mode provides its own MCP tools with SDK-compatible parameter names. Passthrough mode doesn't need this since OpenCode handles tool execution directly.
|
|
262
|
+
</details>
|
|
186
263
|
|
|
187
264
|
## Troubleshooting
|
|
188
265
|
|
|
189
|
-
|
|
266
|
+
| Problem | Solution |
|
|
267
|
+
|---------|----------|
|
|
268
|
+
| "Authentication failed" | Run `claude login` to authenticate |
|
|
269
|
+
| "Connection refused" | Make sure the proxy is running: `bun run proxy` |
|
|
270
|
+
| "Port 3456 is already in use" | `kill $(lsof -ti :3456)` or use `CLAUDE_PROXY_PORT=4567` |
|
|
271
|
+
| Title generation fails | Set `"small_model": "anthropic/claude-haiku-4-5"` in your OpenCode config |
|
|
272
|
+
|
|
273
|
+
## Auto-start (macOS)
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
cat > ~/Library/LaunchAgents/com.claude-max-proxy.plist << EOF
|
|
277
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
278
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
279
|
+
<plist version="1.0">
|
|
280
|
+
<dict>
|
|
281
|
+
<key>Label</key>
|
|
282
|
+
<string>com.claude-max-proxy</string>
|
|
283
|
+
<key>ProgramArguments</key>
|
|
284
|
+
<array>
|
|
285
|
+
<string>$(pwd)/bin/claude-proxy-supervisor.sh</string>
|
|
286
|
+
</array>
|
|
287
|
+
<key>WorkingDirectory</key>
|
|
288
|
+
<string>$(pwd)</string>
|
|
289
|
+
<key>EnvironmentVariables</key>
|
|
290
|
+
<dict>
|
|
291
|
+
<key>CLAUDE_PROXY_PASSTHROUGH</key>
|
|
292
|
+
<string>1</string>
|
|
293
|
+
</dict>
|
|
294
|
+
<key>RunAtLoad</key>
|
|
295
|
+
<true/>
|
|
296
|
+
<key>KeepAlive</key>
|
|
297
|
+
<true/>
|
|
298
|
+
</dict>
|
|
299
|
+
</plist>
|
|
300
|
+
EOF
|
|
301
|
+
|
|
302
|
+
launchctl load ~/Library/LaunchAgents/com.claude-max-proxy.plist
|
|
303
|
+
```
|
|
190
304
|
|
|
191
|
-
|
|
305
|
+
## Testing
|
|
192
306
|
|
|
193
|
-
|
|
307
|
+
```bash
|
|
308
|
+
bun test
|
|
309
|
+
```
|
|
194
310
|
|
|
195
|
-
|
|
311
|
+
106 tests across 13 files covering:
|
|
312
|
+
- Passthrough tool forwarding and tool_result acceptance
|
|
313
|
+
- PreToolUse hook interception and agent name fuzzy matching
|
|
314
|
+
- SDK agent definition extraction (native OpenCode + oh-my-opencode)
|
|
315
|
+
- MCP tool filtering (internal mode)
|
|
316
|
+
- Session resume (header-based and fingerprint-based)
|
|
317
|
+
- Streaming message deduplication
|
|
318
|
+
- Working directory propagation
|
|
319
|
+
- Concurrent request handling
|
|
320
|
+
- Error classification (auth, rate limit, billing, timeout)
|
|
196
321
|
|
|
197
|
-
###
|
|
322
|
+
### Health Endpoint
|
|
198
323
|
|
|
199
|
-
|
|
324
|
+
```bash
|
|
325
|
+
curl http://127.0.0.1:3456/health
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Returns auth status, subscription type, and proxy mode. Use this to verify the proxy is running and authenticated before connecting OpenCode.
|
|
329
|
+
|
|
330
|
+
## Architecture
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
src/
|
|
334
|
+
├── proxy/
|
|
335
|
+
│ ├── server.ts # HTTP server, passthrough/internal modes, SSE streaming, session resume
|
|
336
|
+
│ ├── agentDefs.ts # Extract SDK agent definitions from OpenCode's Task tool
|
|
337
|
+
│ ├── agentMatch.ts # Fuzzy matching for agent names (6-level priority)
|
|
338
|
+
│ └── types.ts # ProxyConfig types and defaults
|
|
339
|
+
├── mcpTools.ts # MCP tool definitions for internal mode (read, write, edit, bash, glob, grep)
|
|
340
|
+
├── logger.ts # Structured logging with AsyncLocalStorage context
|
|
341
|
+
├── plugin/
|
|
342
|
+
│ └── claude-max-headers.ts # OpenCode plugin for session header injection
|
|
343
|
+
└── __tests__/ # 106 tests across 13 files
|
|
344
|
+
├── helpers.ts
|
|
345
|
+
├── integration.test.ts
|
|
346
|
+
├── proxy-agent-definitions.test.ts
|
|
347
|
+
├── proxy-agent-fuzzy-match.test.ts
|
|
348
|
+
├── proxy-error-handling.test.ts
|
|
349
|
+
├── proxy-mcp-filtering.test.ts
|
|
350
|
+
├── proxy-passthrough-concept.test.ts
|
|
351
|
+
├── proxy-pretooluse-hook.test.ts
|
|
352
|
+
├── proxy-session-resume.test.ts
|
|
353
|
+
├── proxy-streaming-message.test.ts
|
|
354
|
+
├── proxy-subagent-support.test.ts
|
|
355
|
+
├── proxy-tool-forwarding.test.ts
|
|
356
|
+
├── proxy-transparent-tools.test.ts
|
|
357
|
+
└── proxy-working-directory.test.ts
|
|
358
|
+
```
|
|
200
359
|
|
|
201
360
|
## License
|
|
202
361
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Auto-restart supervisor for claude-max-proxy
|
|
3
|
+
#
|
|
4
|
+
# The Claude Agent SDK's cli.js subprocess (compiled with Bun) can crash
|
|
5
|
+
# during cleanup of concurrent streaming responses — a known Bun bug
|
|
6
|
+
# (oven-sh/bun#17947). All responses are delivered correctly; the crash
|
|
7
|
+
# only occurs after response completion.
|
|
8
|
+
#
|
|
9
|
+
# This supervisor runs the proxy in a subshell with signal isolation,
|
|
10
|
+
# detects crashes, and restarts in ~1 second.
|
|
11
|
+
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
cd "$SCRIPT_DIR/.."
|
|
14
|
+
|
|
15
|
+
# Ignore signals that the child's crash might propagate
|
|
16
|
+
trap '' SIGPIPE
|
|
17
|
+
|
|
18
|
+
RESTART_COUNT=0
|
|
19
|
+
MAX_RAPID_RESTARTS=50
|
|
20
|
+
RAPID_WINDOW=60
|
|
21
|
+
LAST_START=0
|
|
22
|
+
|
|
23
|
+
while true; do
|
|
24
|
+
NOW=$(date +%s)
|
|
25
|
+
|
|
26
|
+
if [ $((NOW - LAST_START)) -gt $RAPID_WINDOW ]; then
|
|
27
|
+
RESTART_COUNT=0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
if [ $RESTART_COUNT -ge $MAX_RAPID_RESTARTS ]; then
|
|
31
|
+
echo "[supervisor] Too many restarts ($RESTART_COUNT in ${RAPID_WINDOW}s). Stopping."
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
LAST_START=$NOW
|
|
36
|
+
RESTART_COUNT=$((RESTART_COUNT + 1))
|
|
37
|
+
|
|
38
|
+
if [ $RESTART_COUNT -gt 1 ]; then
|
|
39
|
+
echo "[supervisor] Restarting proxy (restart #$RESTART_COUNT)..."
|
|
40
|
+
else
|
|
41
|
+
echo "[supervisor] Starting proxy..."
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Run in subshell so crashes don't kill the supervisor
|
|
45
|
+
(exec bun run ./bin/claude-proxy.ts)
|
|
46
|
+
EXIT_CODE=$?
|
|
47
|
+
|
|
48
|
+
if [ $EXIT_CODE -eq 0 ]; then
|
|
49
|
+
echo "[supervisor] Proxy exited cleanly."
|
|
50
|
+
break
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Signal-based exits (128+signal)
|
|
54
|
+
if [ $EXIT_CODE -gt 128 ]; then
|
|
55
|
+
SIG=$((EXIT_CODE - 128))
|
|
56
|
+
echo "[supervisor] Proxy killed by signal $SIG. Restarting in 1s..."
|
|
57
|
+
else
|
|
58
|
+
echo "[supervisor] Proxy exited (code $EXIT_CODE). Restarting in 1s..."
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
sleep 1
|
|
62
|
+
done
|
package/bin/claude-proxy.ts
CHANGED
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { startProxyServer } from "../src/proxy/server"
|
|
4
|
+
import { execSync } from "child_process"
|
|
5
|
+
|
|
6
|
+
// Prevent SDK subprocess crashes from killing the proxy
|
|
7
|
+
process.on("uncaughtException", (err) => {
|
|
8
|
+
console.error(`[PROXY] Uncaught exception (recovered): ${err.message}`)
|
|
9
|
+
})
|
|
10
|
+
process.on("unhandledRejection", (reason) => {
|
|
11
|
+
console.error(`[PROXY] Unhandled rejection (recovered): ${reason instanceof Error ? reason.message : reason}`)
|
|
12
|
+
})
|
|
4
13
|
|
|
5
14
|
const port = parseInt(process.env.CLAUDE_PROXY_PORT || "3456", 10)
|
|
6
15
|
const host = process.env.CLAUDE_PROXY_HOST || "127.0.0.1"
|
|
16
|
+
const idleTimeoutSeconds = parseInt(process.env.CLAUDE_PROXY_IDLE_TIMEOUT_SECONDS || "120", 10)
|
|
17
|
+
|
|
18
|
+
// Pre-flight auth check
|
|
19
|
+
try {
|
|
20
|
+
const authJson = execSync("claude auth status", { encoding: "utf-8", timeout: 5000 })
|
|
21
|
+
const auth = JSON.parse(authJson)
|
|
22
|
+
if (!auth.loggedIn) {
|
|
23
|
+
console.error("\x1b[31m✗ Not logged in to Claude.\x1b[0m Run: claude login")
|
|
24
|
+
process.exit(1)
|
|
25
|
+
}
|
|
26
|
+
if (auth.subscriptionType !== "max") {
|
|
27
|
+
console.error(`\x1b[33m⚠ Claude subscription: ${auth.subscriptionType || "unknown"} (Max recommended)\x1b[0m`)
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
console.error("\x1b[33m⚠ Could not verify Claude auth status. If requests fail, run: claude login\x1b[0m")
|
|
31
|
+
}
|
|
7
32
|
|
|
8
|
-
await startProxyServer({ port, host })
|
|
33
|
+
await startProxyServer({ port, host, idleTimeoutSeconds })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-claude-max-proxy",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Use your Claude Max subscription with OpenCode via proxy server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/proxy/server.ts",
|
|
@@ -11,14 +11,17 @@
|
|
|
11
11
|
".": "./src/proxy/server.ts"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"start": "
|
|
15
|
-
"proxy": "
|
|
14
|
+
"start": "./bin/claude-proxy-supervisor.sh",
|
|
15
|
+
"proxy": "./bin/claude-proxy-supervisor.sh",
|
|
16
16
|
"test": "bun test",
|
|
17
|
-
"typecheck": "tsc --noEmit"
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"proxy:direct": "bun run ./bin/claude-proxy.ts"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.
|
|
21
|
-
"
|
|
21
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.80",
|
|
22
|
+
"glob": "^13.0.0",
|
|
23
|
+
"hono": "^4.11.4",
|
|
24
|
+
"p-queue": "^8.0.1"
|
|
22
25
|
},
|
|
23
26
|
"devDependencies": {
|
|
24
27
|
"@types/bun": "^1.2.21",
|