@rynfar/meridian 1.24.5 → 1.26.5
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 +144 -195
- package/dist/cli-a05ws7rb.js +18 -0
- package/dist/{cli-jd4atcxs.js → cli-m9pfb7h9.js} +1 -18
- package/dist/cli-rtab0qa6.js +67 -0
- package/dist/{cli-9pc43rfa.js → cli-yve9q0a0.js} +471 -66
- package/dist/cli.js +39 -3
- package/dist/proxy/adapter.d.ts +1 -1
- package/dist/proxy/fileChanges.d.ts.map +1 -1
- package/dist/proxy/models.d.ts +17 -1
- package/dist/proxy/models.d.ts.map +1 -1
- package/dist/proxy/openai.d.ts +142 -0
- package/dist/proxy/openai.d.ts.map +1 -0
- package/dist/proxy/query.d.ts +23 -63
- package/dist/proxy/query.d.ts.map +1 -1
- package/dist/proxy/server.d.ts.map +1 -1
- package/dist/proxy/session/cache.d.ts +9 -4
- package/dist/proxy/session/cache.d.ts.map +1 -1
- package/dist/proxy/session/lineage.d.ts +9 -0
- package/dist/proxy/session/lineage.d.ts.map +1 -1
- package/dist/proxy/sessionStore.d.ts +5 -1
- package/dist/proxy/sessionStore.d.ts.map +1 -1
- package/dist/proxy/setup.d.ts +42 -0
- package/dist/proxy/setup.d.ts.map +1 -0
- package/dist/server.js +4 -2
- package/dist/setup-5x116vbs.js +13 -0
- package/dist/telemetry/dashboard.d.ts +1 -1
- package/dist/telemetry/dashboard.d.ts.map +1 -1
- package/dist/telemetry/routes.d.ts.map +1 -1
- package/dist/{tokenRefresh-wzn2bvrq.js → tokenRefresh-ywwpe8k2.js} +2 -1
- package/package.json +4 -3
- package/plugin/meridian.ts +54 -0
package/README.md
CHANGED
|
@@ -11,9 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
Meridian turns your Claude Max subscription into a local Anthropic API. Any tool that speaks the Anthropic protocol — OpenCode, Crush, Cline,
|
|
15
|
-
|
|
16
|
-
Harness Claude, your way.
|
|
14
|
+
Meridian turns your Claude Max subscription into a local Anthropic API. Any tool that speaks the Anthropic or OpenAI protocol — OpenCode, Crush, Cline, Aider, Open WebUI — connects to Meridian and gets Claude, powered by your existing subscription through the official Claude Code SDK.
|
|
17
15
|
|
|
18
16
|
> [!NOTE]
|
|
19
17
|
> **Renamed from `opencode-claude-max-proxy`.** If you're upgrading, see [`MIGRATION.md`](MIGRATION.md) for the checklist. Your existing sessions, env vars, and agent configs all continue to work.
|
|
@@ -21,17 +19,20 @@ Harness Claude, your way.
|
|
|
21
19
|
## Quick Start
|
|
22
20
|
|
|
23
21
|
```bash
|
|
24
|
-
# Install
|
|
22
|
+
# 1. Install
|
|
25
23
|
npm install -g @rynfar/meridian
|
|
26
24
|
|
|
27
|
-
# Authenticate (one time)
|
|
25
|
+
# 2. Authenticate (one time)
|
|
28
26
|
claude login
|
|
29
27
|
|
|
30
|
-
#
|
|
28
|
+
# 3. Configure OpenCode plugin (one time — OpenCode users only)
|
|
29
|
+
meridian setup
|
|
30
|
+
|
|
31
|
+
# 4. Start
|
|
31
32
|
meridian
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
Meridian
|
|
35
|
+
Meridian runs on `http://127.0.0.1:3456`. Point any Anthropic-compatible tool at it:
|
|
35
36
|
|
|
36
37
|
```bash
|
|
37
38
|
ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://127.0.0.1:3456 opencode
|
|
@@ -43,7 +44,7 @@ The API key value doesn't matter — Meridian authenticates through your Claude
|
|
|
43
44
|
|
|
44
45
|
You're paying for Claude Max. It includes programmatic access through the Claude Code SDK. But your favorite coding tools expect an Anthropic API endpoint and an API key.
|
|
45
46
|
|
|
46
|
-
Meridian bridges that gap. It runs locally, accepts standard Anthropic API requests, and routes them through the SDK using your Max subscription.
|
|
47
|
+
Meridian bridges that gap. It runs locally, accepts standard Anthropic API requests, and routes them through the SDK using your Max subscription.
|
|
47
48
|
|
|
48
49
|
<p align="center">
|
|
49
50
|
<img src="assets/how-it-works.svg" alt="How Meridian works" width="920"/>
|
|
@@ -51,25 +52,46 @@ Meridian bridges that gap. It runs locally, accepts standard Anthropic API reque
|
|
|
51
52
|
|
|
52
53
|
## Features
|
|
53
54
|
|
|
54
|
-
- **Standard Anthropic API** — drop-in compatible with any tool that supports custom `base_url`
|
|
55
|
+
- **Standard Anthropic API** — drop-in compatible with any tool that supports a custom `base_url`
|
|
56
|
+
- **OpenAI-compatible API** — `/v1/chat/completions` and `/v1/models` for tools that only speak the OpenAI protocol (Open WebUI, Continue, etc.) — no LiteLLM needed
|
|
55
57
|
- **Session management** — conversations persist across requests, survive compaction and undo, resume after proxy restarts
|
|
56
58
|
- **Streaming** — full SSE streaming with MCP tool filtering
|
|
57
|
-
- **Concurrent sessions** — run parent
|
|
59
|
+
- **Concurrent sessions** — run parent and subagent requests in parallel
|
|
60
|
+
- **Subagent model selection** — primary agents get 1M context; subagents get 200k, preserving rate-limit budget
|
|
61
|
+
- **Auto token refresh** — expired OAuth tokens are refreshed automatically; requests continue without interruption
|
|
58
62
|
- **Passthrough mode** — forward tool calls to the client instead of executing internally
|
|
59
63
|
- **Multimodal** — images, documents, and file attachments pass through to Claude
|
|
60
64
|
- **Telemetry dashboard** — real-time performance metrics at `/telemetry`
|
|
61
|
-
- **Cross-proxy resume** — sessions persist to disk and survive restarts
|
|
62
|
-
- **Agent adapter pattern** — extensible architecture for supporting new agent protocols
|
|
63
65
|
|
|
64
66
|
## Agent Setup
|
|
65
67
|
|
|
66
68
|
### OpenCode
|
|
67
69
|
|
|
70
|
+
**Step 1: Run `meridian setup` (required, one time)**
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
meridian setup
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This adds the Meridian plugin to your OpenCode global config (`~/.config/opencode/opencode.json`). The plugin enables:
|
|
77
|
+
|
|
78
|
+
- **Session tracking** — reliable conversation continuity across requests
|
|
79
|
+
- **Subagent model selection** — primary agents use `sonnet[1m]`; subagents automatically use `sonnet` (200k), preserving your 1M context rate-limit budget
|
|
80
|
+
|
|
81
|
+
If the plugin is missing, Meridian warns at startup and reports `"plugin": "not-configured"` in the health endpoint.
|
|
82
|
+
|
|
83
|
+
**Step 2: Start**
|
|
84
|
+
|
|
68
85
|
```bash
|
|
69
86
|
ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://127.0.0.1:3456 opencode
|
|
70
87
|
```
|
|
71
88
|
|
|
72
|
-
|
|
89
|
+
Or set these in your shell profile so they're always active:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
export ANTHROPIC_API_KEY=x
|
|
93
|
+
export ANTHROPIC_BASE_URL=http://127.0.0.1:3456
|
|
94
|
+
```
|
|
73
95
|
|
|
74
96
|
### Crush
|
|
75
97
|
|
|
@@ -94,75 +116,38 @@ Add a provider to `~/.config/crush/crush.json`:
|
|
|
94
116
|
}
|
|
95
117
|
```
|
|
96
118
|
|
|
97
|
-
Then use Meridian models in Crush:
|
|
98
|
-
|
|
99
119
|
```bash
|
|
100
120
|
crush run --model meridian/claude-sonnet-4-6 "refactor this function"
|
|
101
121
|
crush --model meridian/claude-opus-4-6 # interactive TUI
|
|
102
122
|
```
|
|
103
123
|
|
|
104
|
-
Crush is automatically detected from its `Charm-Crush/` User-Agent — no
|
|
124
|
+
Crush is automatically detected from its `Charm-Crush/` User-Agent — no plugin needed.
|
|
105
125
|
|
|
106
126
|
### Droid (Factory AI)
|
|
107
127
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
**1. Add Meridian as a custom model provider** in `~/.factory/settings.json`:
|
|
128
|
+
Add Meridian as a custom model provider in `~/.factory/settings.json`:
|
|
111
129
|
|
|
112
130
|
```json
|
|
113
131
|
{
|
|
114
132
|
"customModels": [
|
|
115
|
-
{
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
"provider": "anthropic",
|
|
119
|
-
"baseUrl": "http://127.0.0.1:3456",
|
|
120
|
-
"apiKey": "x"
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
"model": "claude-opus-4-6",
|
|
124
|
-
"name": "Opus 4.6 (1M — Meridian)",
|
|
125
|
-
"provider": "anthropic",
|
|
126
|
-
"baseUrl": "http://127.0.0.1:3456",
|
|
127
|
-
"apiKey": "x"
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
"model": "claude-haiku-4-5-20251001",
|
|
131
|
-
"name": "Haiku 4.5 (Meridian)",
|
|
132
|
-
"provider": "anthropic",
|
|
133
|
-
"baseUrl": "http://127.0.0.1:3456",
|
|
134
|
-
"apiKey": "x"
|
|
135
|
-
}
|
|
133
|
+
{ "model": "claude-sonnet-4-6", "name": "Sonnet 4.6 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
|
|
134
|
+
{ "model": "claude-opus-4-6", "name": "Opus 4.6 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
|
|
135
|
+
{ "model": "claude-haiku-4-5-20251001", "name": "Haiku 4.5 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" }
|
|
136
136
|
]
|
|
137
137
|
}
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
**2. In the Droid TUI**, open the model selector (`/model`) and choose any `custom:claude-*` model.
|
|
143
|
-
|
|
144
|
-
**How models map to Claude Max tiers:**
|
|
145
|
-
|
|
146
|
-
| Model name in config | Claude Max tier |
|
|
147
|
-
|---|---|
|
|
148
|
-
| `claude-sonnet-4-6` | `sonnet[1m]` — Sonnet 4.6 with 1M context |
|
|
149
|
-
| `claude-opus-4-6` | `opus[1m]` — Opus 4.6 with 1M context |
|
|
150
|
-
| `claude-haiku-4-5-20251001` | `haiku` — Haiku 4.5 |
|
|
151
|
-
| `claude-sonnet-4-5-*` | `sonnet` — Sonnet 4.5, no extended context |
|
|
152
|
-
|
|
153
|
-
> **Note:** Droid automatically uses Meridian's internal tool execution mode regardless of the global `CLAUDE_PROXY_PASSTHROUGH` setting. No extra configuration needed.
|
|
140
|
+
Then pick any `custom:claude-*` model in the Droid TUI. No plugin needed — Droid is automatically detected.
|
|
154
141
|
|
|
155
142
|
### Cline
|
|
156
143
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
**1. Authenticate Cline with the Anthropic provider:**
|
|
144
|
+
**1. Authenticate:**
|
|
160
145
|
|
|
161
146
|
```bash
|
|
162
147
|
cline auth --provider anthropic --apikey "dummy" --modelid "claude-sonnet-4-6"
|
|
163
148
|
```
|
|
164
149
|
|
|
165
|
-
**2.
|
|
150
|
+
**2. Set the proxy URL** in `~/.cline/data/globalState.json`:
|
|
166
151
|
|
|
167
152
|
```json
|
|
168
153
|
{
|
|
@@ -172,81 +157,93 @@ cline auth --provider anthropic --apikey "dummy" --modelid "claude-sonnet-4-6"
|
|
|
172
157
|
}
|
|
173
158
|
```
|
|
174
159
|
|
|
175
|
-
**3. Run
|
|
160
|
+
**3. Run:**
|
|
176
161
|
|
|
177
162
|
```bash
|
|
178
|
-
cline --yolo "refactor the login function"
|
|
179
|
-
cline --yolo --model claude-opus-4-6 "review this codebase" # opus
|
|
180
|
-
cline --yolo --model claude-haiku-4-5-20251001 "quick question" # haiku (fastest)
|
|
163
|
+
cline --yolo "refactor the login function"
|
|
181
164
|
```
|
|
182
165
|
|
|
183
|
-
No
|
|
166
|
+
No plugin needed — Cline uses the standard Anthropic SDK.
|
|
184
167
|
|
|
185
168
|
### Aider
|
|
186
169
|
|
|
187
|
-
Aider works out of the box — no plugin or config file needed:
|
|
188
|
-
|
|
189
170
|
```bash
|
|
190
171
|
ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://127.0.0.1:3456 \
|
|
191
172
|
aider --model anthropic/claude-sonnet-4-5-20250929
|
|
192
173
|
```
|
|
193
174
|
|
|
194
|
-
|
|
175
|
+
> **Note:** `--no-stream` is incompatible due to a litellm parsing issue — use the default streaming mode.
|
|
176
|
+
|
|
177
|
+
### OpenAI-compatible tools (Open WebUI, Continue, etc.)
|
|
178
|
+
|
|
179
|
+
Meridian speaks the OpenAI protocol natively — no LiteLLM or translation proxy needed.
|
|
180
|
+
|
|
181
|
+
**`POST /v1/chat/completions`** — accepts OpenAI chat format, returns OpenAI completion format (streaming and non-streaming)
|
|
182
|
+
|
|
183
|
+
**`GET /v1/models`** — returns available Claude models in OpenAI format
|
|
184
|
+
|
|
185
|
+
Point any OpenAI-compatible tool at `http://127.0.0.1:3456` with any API key value:
|
|
195
186
|
|
|
196
|
-
|
|
187
|
+
```bash
|
|
188
|
+
# Open WebUI: set OpenAI API base to http://127.0.0.1:3456, API key to any value
|
|
189
|
+
# Continue: set apiBase to http://127.0.0.1:3456 with provider: openai
|
|
190
|
+
# Any OpenAI SDK: set base_url="http://127.0.0.1:3456", api_key="dummy"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
> **Note:** Multi-turn conversations work by packing prior turns into the system prompt. Each request is a fresh SDK session — OpenAI clients replay full history themselves and don't use Meridian's session resumption.
|
|
197
194
|
|
|
198
195
|
### Any Anthropic-compatible tool
|
|
199
196
|
|
|
200
197
|
```bash
|
|
201
198
|
export ANTHROPIC_API_KEY=x
|
|
202
199
|
export ANTHROPIC_BASE_URL=http://127.0.0.1:3456
|
|
203
|
-
# Then start your tool normally
|
|
204
200
|
```
|
|
205
201
|
|
|
206
202
|
## Tested Agents
|
|
207
203
|
|
|
208
|
-
| Agent | Status |
|
|
209
|
-
|
|
210
|
-
| [OpenCode](https://github.com/anomalyco/opencode) | ✅ Verified |
|
|
211
|
-
| [Droid (Factory AI)](https://factory.ai/product/ide) | ✅ Verified | BYOK config (see
|
|
212
|
-
| [Crush](https://github.com/charmbracelet/crush) | ✅ Verified | Provider config (see
|
|
213
|
-
| [Cline](https://github.com/cline/cline) | ✅ Verified | Config (see
|
|
214
|
-
| [
|
|
215
|
-
| [
|
|
204
|
+
| Agent | Status | Notes |
|
|
205
|
+
|-------|--------|-------|
|
|
206
|
+
| [OpenCode](https://github.com/anomalyco/opencode) | ✅ Verified | Requires `meridian setup` — full tool support, session resume, streaming, subagents |
|
|
207
|
+
| [Droid (Factory AI)](https://factory.ai/product/ide) | ✅ Verified | BYOK config (see above) — full tool support, session resume, streaming |
|
|
208
|
+
| [Crush](https://github.com/charmbracelet/crush) | ✅ Verified | Provider config (see above) — full tool support, session resume, headless `crush run` |
|
|
209
|
+
| [Cline](https://github.com/cline/cline) | ✅ Verified | Config (see above) — full tool support, file read/write/edit, bash, session resume |
|
|
210
|
+
| [Aider](https://github.com/paul-gauthier/aider) | ✅ Verified | Env vars — file editing, streaming; `--no-stream` broken (litellm bug) |
|
|
211
|
+
| [Open WebUI](https://github.com/open-webui/open-webui) | ✅ Verified | OpenAI-compatible endpoints — set base URL to `http://127.0.0.1:3456` |
|
|
212
|
+
| [Continue](https://github.com/continuedev/continue) | 🔲 Untested | OpenAI-compatible endpoints should work — set `apiBase` to `http://127.0.0.1:3456` |
|
|
216
213
|
|
|
217
214
|
Tested an agent or built a plugin? [Open an issue](https://github.com/rynfar/meridian/issues) and we'll add it.
|
|
218
215
|
|
|
219
216
|
## Architecture
|
|
220
217
|
|
|
221
|
-
Meridian is built as a modular proxy with clean separation of concerns:
|
|
222
|
-
|
|
223
218
|
```
|
|
224
219
|
src/proxy/
|
|
225
220
|
├── server.ts ← HTTP orchestration (routes, SSE streaming, concurrency)
|
|
226
|
-
├── adapter.ts ← AgentAdapter interface
|
|
221
|
+
├── adapter.ts ← AgentAdapter interface
|
|
227
222
|
├── adapters/
|
|
228
223
|
│ ├── detect.ts ← Agent detection from request headers
|
|
229
224
|
│ ├── opencode.ts ← OpenCode adapter
|
|
230
|
-
│ ├── crush.ts ← Crush
|
|
231
|
-
│ ├── droid.ts ← Droid
|
|
225
|
+
│ ├── crush.ts ← Crush adapter
|
|
226
|
+
│ ├── droid.ts ← Droid adapter
|
|
232
227
|
│ └── passthrough.ts ← LiteLLM passthrough adapter
|
|
233
228
|
├── query.ts ← SDK query options builder
|
|
234
229
|
├── errors.ts ← Error classification
|
|
235
|
-
├── models.ts ← Model mapping (sonnet/opus/haiku)
|
|
236
|
-
├──
|
|
237
|
-
├──
|
|
230
|
+
├── models.ts ← Model mapping (sonnet/opus/haiku, agentMode)
|
|
231
|
+
├── tokenRefresh.ts ← Cross-platform OAuth token refresh
|
|
232
|
+
├── openai.ts ← OpenAI ↔ Anthropic format translation (pure)
|
|
233
|
+
├── setup.ts ← OpenCode plugin configuration
|
|
238
234
|
├── session/
|
|
239
235
|
│ ├── lineage.ts ← Per-message hashing, mutation classification (pure)
|
|
240
236
|
│ ├── fingerprint.ts ← Conversation fingerprinting
|
|
241
237
|
│ └── cache.ts ← LRU session caches
|
|
242
238
|
├── sessionStore.ts ← Cross-proxy file-based session persistence
|
|
243
|
-
├── agentDefs.ts ← Subagent definition extraction
|
|
244
239
|
└── passthroughTools.ts ← Tool forwarding mode
|
|
240
|
+
plugin/
|
|
241
|
+
└── meridian.ts ← OpenCode plugin (session headers + agent mode)
|
|
245
242
|
```
|
|
246
243
|
|
|
247
244
|
### Session Management
|
|
248
245
|
|
|
249
|
-
|
|
246
|
+
Every incoming request is classified:
|
|
250
247
|
|
|
251
248
|
| Classification | What Happened | Action |
|
|
252
249
|
|---------------|---------------|--------|
|
|
@@ -257,30 +254,9 @@ Sessions map agent conversations to Claude SDK sessions. Meridian classifies eve
|
|
|
257
254
|
|
|
258
255
|
Sessions are stored in-memory (LRU) and persisted to `~/.cache/meridian/sessions.json` for cross-proxy resume.
|
|
259
256
|
|
|
260
|
-
###
|
|
261
|
-
|
|
262
|
-
Implement the `AgentAdapter` interface in `src/proxy/adapters/`:
|
|
257
|
+
### Agent Detection
|
|
263
258
|
|
|
264
|
-
|
|
265
|
-
interface AgentAdapter {
|
|
266
|
-
// Required
|
|
267
|
-
getSessionId(c: Context): string | undefined
|
|
268
|
-
extractWorkingDirectory(body: any): string | undefined
|
|
269
|
-
normalizeContent(content: any): string
|
|
270
|
-
getBlockedBuiltinTools(): readonly string[]
|
|
271
|
-
getAgentIncompatibleTools(): readonly string[]
|
|
272
|
-
getMcpServerName(): string
|
|
273
|
-
getAllowedMcpTools(): readonly string[]
|
|
274
|
-
|
|
275
|
-
// Optional
|
|
276
|
-
buildSdkAgents?(body: any, mcpToolNames: readonly string[]): Record<string, any>
|
|
277
|
-
buildSdkHooks?(body: any, sdkAgents: Record<string, any>): any
|
|
278
|
-
buildSystemContextAddendum?(body: any, sdkAgents: Record<string, any>): string
|
|
279
|
-
usesPassthrough?(): boolean // overrides CLAUDE_PROXY_PASSTHROUGH per-agent
|
|
280
|
-
}
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
Agent detection is automatic from the `User-Agent` header:
|
|
259
|
+
Agents are identified from request headers automatically:
|
|
284
260
|
|
|
285
261
|
| User-Agent prefix | Adapter |
|
|
286
262
|
|---|---|
|
|
@@ -288,7 +264,9 @@ Agent detection is automatic from the `User-Agent` header:
|
|
|
288
264
|
| `factory-cli/` | Droid |
|
|
289
265
|
| *(anything else)* | OpenCode (default) |
|
|
290
266
|
|
|
291
|
-
|
|
267
|
+
### Adding a New Agent
|
|
268
|
+
|
|
269
|
+
Implement the `AgentAdapter` interface in `src/proxy/adapters/`. See [`adapters/opencode.ts`](src/proxy/adapters/opencode.ts) for a reference.
|
|
292
270
|
|
|
293
271
|
## Configuration
|
|
294
272
|
|
|
@@ -304,103 +282,75 @@ See [`adapters/detect.ts`](src/proxy/adapters/detect.ts) and [`adapters/opencode
|
|
|
304
282
|
| `MERIDIAN_IDLE_TIMEOUT_SECONDS` | `CLAUDE_PROXY_IDLE_TIMEOUT_SECONDS` | `120` | HTTP keep-alive timeout |
|
|
305
283
|
| `MERIDIAN_TELEMETRY_SIZE` | `CLAUDE_PROXY_TELEMETRY_SIZE` | `1000` | Telemetry ring buffer size |
|
|
306
284
|
| `MERIDIAN_NO_FILE_CHANGES` | `CLAUDE_PROXY_NO_FILE_CHANGES` | unset | Disable "Files changed" summary in responses |
|
|
307
|
-
| `MERIDIAN_SONNET_MODEL` | `CLAUDE_PROXY_SONNET_MODEL` | `sonnet[1m]`* | Force sonnet tier: `sonnet` (200k) or `sonnet[1m]` (1M). Set to `sonnet` if you hit 1M context rate limits
|
|
285
|
+
| `MERIDIAN_SONNET_MODEL` | `CLAUDE_PROXY_SONNET_MODEL` | `sonnet[1m]`* | Force sonnet tier: `sonnet` (200k) or `sonnet[1m]` (1M). Set to `sonnet` if you hit 1M context rate limits |
|
|
308
286
|
|
|
309
|
-
*`sonnet[1m]`
|
|
310
|
-
|
|
311
|
-
## Programmatic API
|
|
312
|
-
|
|
313
|
-
Meridian can be used as a library for building agent plugins and integrations.
|
|
314
|
-
|
|
315
|
-
```typescript
|
|
316
|
-
import { startProxyServer } from "@rynfar/meridian"
|
|
317
|
-
|
|
318
|
-
// Start a proxy instance
|
|
319
|
-
const instance = await startProxyServer({
|
|
320
|
-
port: 3456,
|
|
321
|
-
host: "127.0.0.1",
|
|
322
|
-
silent: true, // suppress console output
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
// instance.config — resolved ProxyConfig
|
|
326
|
-
// instance.server — underlying http.Server
|
|
327
|
-
|
|
328
|
-
// Shut down cleanly
|
|
329
|
-
await instance.close()
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
### Session Header Contract
|
|
333
|
-
|
|
334
|
-
For reliable session tracking, agents should send a session identifier via HTTP header. Without it, the proxy falls back to fingerprint-based matching (hashing the first user message + working directory), which is less reliable.
|
|
335
|
-
|
|
336
|
-
| Header | Purpose |
|
|
337
|
-
|--------|---------|
|
|
338
|
-
| `x-opencode-session` | Maps agent conversations to Claude SDK sessions for resume, undo, and compaction |
|
|
339
|
-
|
|
340
|
-
The proxy uses this header to maintain conversation continuity across requests. Plugin authors should inject it on every request to `/v1/messages`.
|
|
341
|
-
|
|
342
|
-
### Plugin Architecture
|
|
343
|
-
|
|
344
|
-
Meridian is the proxy. Plugins live in the agent's ecosystem.
|
|
345
|
-
|
|
346
|
-
```
|
|
347
|
-
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
348
|
-
│ Agent │ HTTP │ Meridian │ SDK │ Claude Max │
|
|
349
|
-
│ (OpenCode, │────────▶│ Proxy │────────▶│ │
|
|
350
|
-
│ Crush, etc) │◀────────│ │◀────────│ │
|
|
351
|
-
└──────────────┘ └──────────────┘ └──────────────┘
|
|
352
|
-
│
|
|
353
|
-
│ plugin injects headers,
|
|
354
|
-
│ manages proxy lifecycle
|
|
355
|
-
│
|
|
356
|
-
┌──────────────┐
|
|
357
|
-
│ Agent Plugin │
|
|
358
|
-
│ (optional) │
|
|
359
|
-
└──────────────┘
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
A plugin's job is to:
|
|
363
|
-
1. Start/stop a Meridian instance (`startProxyServer` / `instance.close()`)
|
|
364
|
-
2. Inject session headers into outgoing requests
|
|
365
|
-
3. Check proxy health (`GET /health`)
|
|
366
|
-
|
|
367
|
-
See [`examples/opencode-plugin/`](examples/opencode-plugin/) for a reference implementation.
|
|
287
|
+
*`sonnet[1m]` requires Max subscription with Extra Usage enabled. Falls back to `sonnet` automatically if not available.
|
|
368
288
|
|
|
369
289
|
## Endpoints
|
|
370
290
|
|
|
371
291
|
| Endpoint | Description |
|
|
372
292
|
|----------|-------------|
|
|
373
|
-
| `GET /` | Landing page
|
|
293
|
+
| `GET /` | Landing page |
|
|
374
294
|
| `POST /v1/messages` | Anthropic Messages API |
|
|
375
295
|
| `POST /messages` | Alias for `/v1/messages` |
|
|
376
|
-
| `
|
|
296
|
+
| `POST /v1/chat/completions` | OpenAI-compatible chat completions |
|
|
297
|
+
| `GET /v1/models` | OpenAI-compatible model list |
|
|
298
|
+
| `GET /health` | Auth status, mode, plugin status |
|
|
377
299
|
| `POST /auth/refresh` | Manually refresh the OAuth token |
|
|
378
300
|
| `GET /telemetry` | Performance dashboard |
|
|
379
301
|
| `GET /telemetry/requests` | Recent request metrics (JSON) |
|
|
380
302
|
| `GET /telemetry/summary` | Aggregate statistics (JSON) |
|
|
381
303
|
| `GET /telemetry/logs` | Diagnostic logs (JSON) |
|
|
382
304
|
|
|
383
|
-
|
|
305
|
+
Health response example:
|
|
384
306
|
|
|
385
|
-
```
|
|
386
|
-
|
|
307
|
+
```json
|
|
308
|
+
{
|
|
309
|
+
"status": "healthy",
|
|
310
|
+
"auth": { "loggedIn": true, "email": "you@example.com", "subscriptionType": "max" },
|
|
311
|
+
"mode": "internal",
|
|
312
|
+
"plugin": { "opencode": "configured" }
|
|
313
|
+
}
|
|
387
314
|
```
|
|
388
315
|
|
|
389
|
-
|
|
316
|
+
`plugin.opencode` is `"configured"` when `meridian setup` has been run, `"not-configured"` otherwise.
|
|
317
|
+
|
|
318
|
+
## CLI Commands
|
|
319
|
+
|
|
320
|
+
| Command | Description |
|
|
321
|
+
|---------|-------------|
|
|
322
|
+
| `meridian` | Start the proxy server |
|
|
323
|
+
| `meridian setup` | Configure the OpenCode plugin in `~/.config/opencode/opencode.json` |
|
|
324
|
+
| `meridian refresh-token` | Manually refresh the Claude OAuth token (exits 0/1) |
|
|
325
|
+
|
|
326
|
+
## Programmatic API
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
import { startProxyServer } from "@rynfar/meridian"
|
|
330
|
+
|
|
331
|
+
const instance = await startProxyServer({
|
|
332
|
+
port: 3456,
|
|
333
|
+
host: "127.0.0.1",
|
|
334
|
+
silent: true,
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
// instance.server — underlying http.Server
|
|
338
|
+
await instance.close()
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Docker
|
|
390
342
|
|
|
391
343
|
```bash
|
|
392
|
-
docker
|
|
344
|
+
docker run -v ~/.claude:/home/claude/.claude -p 3456:3456 meridian
|
|
393
345
|
```
|
|
394
346
|
|
|
395
347
|
## Testing
|
|
396
348
|
|
|
397
349
|
```bash
|
|
398
|
-
npm test
|
|
399
|
-
npm run build
|
|
350
|
+
npm test # unit + integration tests
|
|
351
|
+
npm run build # build with bun + tsc
|
|
400
352
|
```
|
|
401
353
|
|
|
402
|
-
Three test tiers:
|
|
403
|
-
|
|
404
354
|
| Tier | What | Speed |
|
|
405
355
|
|------|------|-------|
|
|
406
356
|
| Unit | Pure functions, no mocks | Fast |
|
|
@@ -410,30 +360,29 @@ Three test tiers:
|
|
|
410
360
|
## FAQ
|
|
411
361
|
|
|
412
362
|
**Is this allowed by Anthropic's terms?**
|
|
413
|
-
Meridian uses the official Claude Code SDK — the same SDK Anthropic publishes
|
|
363
|
+
Meridian uses the official Claude Code SDK — the same SDK Anthropic publishes for programmatic access. It authenticates through your existing Claude Max session using OAuth.
|
|
414
364
|
|
|
415
365
|
**How is this different from using an API key?**
|
|
416
|
-
API keys are billed per token.
|
|
366
|
+
API keys are billed per token. Claude Max is a flat monthly fee. Meridian lets you use that subscription from any compatible tool.
|
|
417
367
|
|
|
418
|
-
**
|
|
419
|
-
|
|
368
|
+
**What happens if my OAuth token expires?**
|
|
369
|
+
Tokens expire roughly every 8 hours. Meridian detects the expiry, refreshes the token automatically, and retries the request — so requests continue transparently. If the refresh fails (e.g. the refresh token has expired after weeks of inactivity), Meridian returns a clear error telling you to run `claude login`.
|
|
420
370
|
|
|
421
|
-
**
|
|
422
|
-
OAuth tokens expire roughly every 8 hours. Meridian detects the expiry on the next request, refreshes the token automatically, and retries — so requests continue to work transparently. If the refresh itself fails (e.g. your refresh token has expired after weeks of inactivity), Meridian returns a clear error telling you to run `claude login`.
|
|
423
|
-
|
|
424
|
-
**Can I trigger a refresh manually?**
|
|
425
|
-
Yes — two options:
|
|
371
|
+
**Can I trigger a token refresh manually?**
|
|
426
372
|
|
|
427
373
|
```bash
|
|
428
|
-
# CLI
|
|
374
|
+
# CLI — works whether the proxy is running or not
|
|
429
375
|
meridian refresh-token
|
|
430
376
|
|
|
431
|
-
# HTTP
|
|
432
|
-
curl -
|
|
433
|
-
# {"success":true,"message":"OAuth token refreshed successfully"}
|
|
377
|
+
# HTTP — while the proxy is running
|
|
378
|
+
curl -X POST http://127.0.0.1:3456/auth/refresh
|
|
434
379
|
```
|
|
435
380
|
|
|
436
|
-
|
|
381
|
+
**I'm hitting rate limits on 1M context. What do I do?**
|
|
382
|
+
Set `MERIDIAN_SONNET_MODEL=sonnet` to use the 200k model for all requests. If you're using OpenCode with the Meridian plugin, subagents already use 200k automatically — only the primary agent uses 1M.
|
|
383
|
+
|
|
384
|
+
**Why does the health endpoint show `"plugin": "not-configured"`?**
|
|
385
|
+
You haven't run `meridian setup`. Without the plugin, OpenCode requests won't have session tracking or subagent model selection. Run `meridian setup` and restart OpenCode.
|
|
437
386
|
|
|
438
387
|
## Contributing
|
|
439
388
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
17
|
+
|
|
18
|
+
export { __export, __require };
|
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __returnValue = (v) => v;
|
|
4
|
-
function __exportSetter(name, newValue) {
|
|
5
|
-
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
-
}
|
|
7
|
-
var __export = (target, all) => {
|
|
8
|
-
for (var name in all)
|
|
9
|
-
__defProp(target, name, {
|
|
10
|
-
get: all[name],
|
|
11
|
-
enumerable: true,
|
|
12
|
-
configurable: true,
|
|
13
|
-
set: __exportSetter.bind(all, name)
|
|
14
|
-
});
|
|
15
|
-
};
|
|
16
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
17
|
-
|
|
18
1
|
// src/proxy/tokenRefresh.ts
|
|
19
2
|
import { execFile as execFileCb } from "child_process";
|
|
20
3
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -217,4 +200,4 @@ function resetInflightRefresh() {
|
|
|
217
200
|
inflightRefresh = null;
|
|
218
201
|
}
|
|
219
202
|
|
|
220
|
-
export {
|
|
203
|
+
export { withClaudeLogContext, claudeLog, createPlatformCredentialStore, refreshOAuthToken, resetInflightRefresh };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/proxy/setup.ts
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { homedir, platform } from "os";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
function findOpencodeConfigPath() {
|
|
7
|
+
if (process.env.OPENCODE_CONFIG_DIR) {
|
|
8
|
+
return join(process.env.OPENCODE_CONFIG_DIR, "opencode.json");
|
|
9
|
+
}
|
|
10
|
+
if (process.env.XDG_CONFIG_HOME) {
|
|
11
|
+
return join(process.env.XDG_CONFIG_HOME, "opencode", "opencode.json");
|
|
12
|
+
}
|
|
13
|
+
if (platform() === "win32" && process.env.APPDATA) {
|
|
14
|
+
return join(process.env.APPDATA, "opencode", "opencode.json");
|
|
15
|
+
}
|
|
16
|
+
return join(homedir(), ".config", "opencode", "opencode.json");
|
|
17
|
+
}
|
|
18
|
+
function findPluginPath(fromUrl) {
|
|
19
|
+
const dir = dirname(fileURLToPath(fromUrl));
|
|
20
|
+
return join(dir, "..", "plugin", "meridian.ts");
|
|
21
|
+
}
|
|
22
|
+
var STALE_PATTERNS = [
|
|
23
|
+
"opencode-claude-max-proxy",
|
|
24
|
+
"claude-max-headers",
|
|
25
|
+
"meridian-agent-mode"
|
|
26
|
+
];
|
|
27
|
+
function isMeridianEntry(entry) {
|
|
28
|
+
return STALE_PATTERNS.some((p) => entry.includes(p)) || entry.includes("meridian.ts") || entry.includes("@rynfar/meridian");
|
|
29
|
+
}
|
|
30
|
+
function checkPluginConfigured(configPath) {
|
|
31
|
+
const path = configPath ?? findOpencodeConfigPath();
|
|
32
|
+
if (!existsSync(path))
|
|
33
|
+
return false;
|
|
34
|
+
try {
|
|
35
|
+
const raw = readFileSync(path, "utf-8");
|
|
36
|
+
const config = JSON.parse(raw);
|
|
37
|
+
const plugins = Array.isArray(config.plugin) ? config.plugin : [];
|
|
38
|
+
return plugins.some((p) => typeof p === "string" && isMeridianEntry(p));
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function runSetup(pluginPath, configPath) {
|
|
44
|
+
const path = configPath ?? findOpencodeConfigPath();
|
|
45
|
+
const dir = dirname(path);
|
|
46
|
+
let config = {};
|
|
47
|
+
let created = false;
|
|
48
|
+
if (existsSync(path)) {
|
|
49
|
+
try {
|
|
50
|
+
config = JSON.parse(readFileSync(path, "utf-8"));
|
|
51
|
+
} catch {}
|
|
52
|
+
} else {
|
|
53
|
+
created = true;
|
|
54
|
+
if (!existsSync(dir))
|
|
55
|
+
mkdirSync(dir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
const existing = Array.isArray(config.plugin) ? config.plugin.filter((p) => typeof p === "string") : [];
|
|
58
|
+
const removedStale = existing.filter(isMeridianEntry);
|
|
59
|
+
const others = existing.filter((p) => !isMeridianEntry(p));
|
|
60
|
+
const alreadyConfigured = removedStale.some((p) => p === pluginPath);
|
|
61
|
+
config.plugin = [...others, pluginPath];
|
|
62
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + `
|
|
63
|
+
`, "utf-8");
|
|
64
|
+
return { configPath: path, pluginPath, alreadyConfigured, removedStale, created };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { findOpencodeConfigPath, findPluginPath, checkPluginConfigured, runSetup };
|