@rynfar/meridian 1.54.0 → 1.55.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 +25 -968
- package/dist/{cli-mqht6rs9.js → cli-w63v1ftp.js} +11 -4
- package/dist/cli.js +1 -1
- package/dist/proxy/adapters/codex.d.ts.map +1 -1
- package/dist/proxy/adapters/pi.d.ts.map +1 -1
- package/dist/proxy/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,106 +49,7 @@ ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://127.0.0.1:3456 opencode
|
|
|
49
49
|
|
|
50
50
|
The API key value is a placeholder — Meridian authenticates through the Claude Code SDK, not API keys. Most Anthropic-compatible tools require this field to be set, but any value works.
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Meridian provides a Nix flake for declarative installation.
|
|
55
|
-
|
|
56
|
-
**Add to your flake inputs:**
|
|
57
|
-
|
|
58
|
-
```nix
|
|
59
|
-
{
|
|
60
|
-
inputs.meridian.url = "github:rynfar/meridian";
|
|
61
|
-
}
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
**Install the package** (via overlay or directly):
|
|
65
|
-
|
|
66
|
-
```nix
|
|
67
|
-
# Option A: overlay
|
|
68
|
-
nixpkgs.overlays = [ meridian.overlays.default ];
|
|
69
|
-
environment.systemPackages = [ pkgs.meridian ];
|
|
70
|
-
|
|
71
|
-
# Option B: direct reference
|
|
72
|
-
environment.systemPackages = [ meridian.packages.${system}.meridian ];
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
**OpenCode plugin** -- the plugin file is included at `${pkgs.meridian}/lib/meridian/plugin/meridian.ts`. Since this path lives in the Nix store, you need to make it available to OpenCode:
|
|
76
|
-
|
|
77
|
-
If you generate your OpenCode config from Nix (e.g. via Home Manager), interpolate the path directly:
|
|
78
|
-
|
|
79
|
-
```nix
|
|
80
|
-
# home-manager example
|
|
81
|
-
xdg.configFile."opencode/opencode.json".text = builtins.toJSON {
|
|
82
|
-
plugin = [ "${pkgs.meridian}/lib/meridian/plugin/meridian.ts" ];
|
|
83
|
-
};
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
If you don't manage your OpenCode config through Nix, symlink the plugin to a stable path and reference that instead:
|
|
87
|
-
|
|
88
|
-
```nix
|
|
89
|
-
# configuration.nix or home-manager
|
|
90
|
-
environment.etc."meridian/plugin/meridian.ts".source =
|
|
91
|
-
"${pkgs.meridian}/lib/meridian/plugin/meridian.ts";
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Then in `~/.config/opencode/opencode.json`:
|
|
95
|
-
|
|
96
|
-
```json
|
|
97
|
-
{ "plugin": ["/etc/meridian/plugin/meridian.ts"] }
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
> **Important:** Do not use `meridian setup` on NixOS. It writes an absolute Nix store path (e.g. `/nix/store/...-meridian-1.x.x/lib/...`) into your OpenCode config, which will break on the next `nixos-rebuild switch` or `home-manager switch` when the store path changes. Use one of the approaches above instead.
|
|
101
|
-
|
|
102
|
-
> **Note:** Meridian's package depends on the unfree `claude-code` from nixpkgs instead of bundling its own binary. The flake accepts the unfree license when it builds the package and exports the finished derivation, so consuming it through the overlay or `packages.<system>.meridian` does not re-run nixpkgs' unfree check and needs no `allowUnfree` setting.
|
|
103
|
-
|
|
104
|
-
**Home Manager service** -- run Meridian as a user systemd service:
|
|
105
|
-
|
|
106
|
-
```nix
|
|
107
|
-
# flake.nix
|
|
108
|
-
{
|
|
109
|
-
inputs.meridian.url = "github:rynfar/meridian";
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
# home-manager config
|
|
113
|
-
{
|
|
114
|
-
imports = [ meridian.homeModules.default ];
|
|
115
|
-
|
|
116
|
-
services.meridian = {
|
|
117
|
-
enable = true;
|
|
118
|
-
settings = {
|
|
119
|
-
port = 3456;
|
|
120
|
-
host = "127.0.0.1";
|
|
121
|
-
# passthrough = true;
|
|
122
|
-
# defaultAgent = "opencode";
|
|
123
|
-
# sonnetModel = "sonnet";
|
|
124
|
-
# Load plugins from the Nix store (rendered to a plugins.json manifest).
|
|
125
|
-
# The official scrub plugins ship prebuilt via the meridian overlay:
|
|
126
|
-
# pluginConfig = [ { path = pkgs.meridianPlugins.opencode-scrub.path; } ];
|
|
127
|
-
# pluginDir = "/path/to/extra/plugins";
|
|
128
|
-
};
|
|
129
|
-
# Extra env vars not covered by settings
|
|
130
|
-
# environment = {
|
|
131
|
-
# MERIDIAN_MAX_CONCURRENT = "20";
|
|
132
|
-
# };
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
The service starts automatically on login. Manage it with `systemctl --user {start,stop,restart,status} meridian`.
|
|
138
|
-
|
|
139
|
-
The module manages only the systemd user service — it does **not** put the `meridian` CLI on your `$PATH`. If you also want to run `meridian` from a shell, add the package yourself:
|
|
140
|
-
|
|
141
|
-
```nix
|
|
142
|
-
home.packages = [ config.services.meridian.package ];
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
The plugin path is also available as `config.services.meridian.opencode.pluginPath` for use in your OpenCode config:
|
|
146
|
-
|
|
147
|
-
```nix
|
|
148
|
-
xdg.configFile."opencode/opencode.json".text = builtins.toJSON {
|
|
149
|
-
plugin = [ config.services.meridian.opencode.pluginPath ];
|
|
150
|
-
};
|
|
151
|
-
```
|
|
52
|
+
Using a different agent, NixOS, or Docker? See the [documentation](#documentation) below.
|
|
152
53
|
|
|
153
54
|
## Why Meridian?
|
|
154
55
|
|
|
@@ -158,6 +59,19 @@ The Claude Agent SDK provides programmatic access to Claude. But your favorite c
|
|
|
158
59
|
<img src="assets/how-it-works.svg" alt="How Meridian works" width="920"/>
|
|
159
60
|
</p>
|
|
160
61
|
|
|
62
|
+
## Documentation
|
|
63
|
+
|
|
64
|
+
| Guide | What's in it |
|
|
65
|
+
|-------|--------------|
|
|
66
|
+
| [Agent Setup](docs/agents.md) | Per-agent config: OpenCode, Crush, Droid, Cline, Aider, Codex CLI, Open WebUI, Cherry Studio, ForgeCode, Pi, Claude Code, Claude Design MCP, adapter instances |
|
|
67
|
+
| [Configuration](docs/configuration.md) | Environment variables, endpoints, API key auth, SDK feature toggles, passthrough mode, CLI commands |
|
|
68
|
+
| [Multi-Profile Support](docs/profiles.md) | Multiple Claude accounts, headless login, sticky session routing |
|
|
69
|
+
| [Deployment](docs/deployment.md) | NixOS / Nix flake, Home Manager service, Docker |
|
|
70
|
+
| [Plugins](docs/plugins.md) | Plugin system and the official scrub plugins |
|
|
71
|
+
| [Development](docs/development.md) | Architecture overview, testing, programmatic API |
|
|
72
|
+
| [`MONITORING.md`](MONITORING.md) | Telemetry, token usage, and prompt cache health |
|
|
73
|
+
| [`ARCHITECTURE.md`](ARCHITECTURE.md) | Module map and dependency rules |
|
|
74
|
+
|
|
161
75
|
## Features
|
|
162
76
|
|
|
163
77
|
- **Standard Anthropic API** — drop-in compatible with any tool that supports a custom `base_url`
|
|
@@ -169,8 +83,8 @@ The Claude Agent SDK provides programmatic access to Claude. But your favorite c
|
|
|
169
83
|
- **Auto token refresh** — expired OAuth tokens are refreshed automatically; requests continue without interruption
|
|
170
84
|
- **Passthrough mode** — forward tool calls to the client instead of executing internally
|
|
171
85
|
- **Multimodal** — images, documents, file attachments, and multimodal tool results pass through to Claude
|
|
172
|
-
- **Multi-profile** — switch between Claude accounts instantly, no restart needed; opt-in [sticky session routing](#sticky-session-routing) distributes sessions across accounts while keeping per-account prompt caches warm
|
|
173
|
-
- **Adapter instances** — run several configurations of the same adapter side by side (per-instance thinking, system prompt, passthrough) selected by header or match rules — see [Adapter instances](#adapter-instances)
|
|
86
|
+
- **Multi-profile** — switch between Claude accounts instantly, no restart needed; opt-in [sticky session routing](docs/profiles.md#sticky-session-routing) distributes sessions across accounts while keeping per-account prompt caches warm
|
|
87
|
+
- **Adapter instances** — run several configurations of the same adapter side by side (per-instance thinking, system prompt, passthrough) selected by header or match rules — see [Adapter instances](docs/agents.md#adapter-instances)
|
|
174
88
|
- **Telemetry dashboard** — real-time performance metrics at `/telemetry`, including token usage and prompt cache efficiency ([`MONITORING.md`](MONITORING.md))
|
|
175
89
|
- **Cost estimation** — estimated API-equivalent value of your traffic, per model and per profile, using current list prices with configurable overrides (`~/.config/meridian/model-pricing.json`, editable at `/settings`)
|
|
176
90
|
- **Envelope integrity auditing** — Meridian validates its own wire output on every response (no dangling blocks, no undelivered or empty tool calls) and surfaces violations on the dashboard
|
|
@@ -178,882 +92,25 @@ The Claude Agent SDK provides programmatic access to Claude. But your favorite c
|
|
|
178
92
|
- **Prometheus metrics** — `GET /metrics` endpoint for scraping request counters and duration histograms
|
|
179
93
|
- **SDK feature toggles** *(experimental)* — unlock Claude Code features (memory, dreaming, CLAUDE.md) for any connected agent
|
|
180
94
|
|
|
181
|
-
## SDK Feature Toggles (Experimental)
|
|
182
|
-
|
|
183
|
-
Meridian can expose Claude Code features to any connected agent. Capabilities like auto-memory, dreaming, and CLAUDE.md — normally exclusive to Claude Code — become available to OpenCode, Crush, Droid, and any other harness routed through Meridian. Each agent keeps its own toolchain while gaining access to these additional features.
|
|
184
|
-
|
|
185
|
-
Configure per-adapter at **`/settings`** in the Meridian web UI. Changes take effect on the next request — no restart needed. Config is persisted to `~/.config/meridian/sdk-features.json`.
|
|
186
|
-
|
|
187
|
-
### Available features
|
|
188
|
-
|
|
189
|
-
| Setting | Options | Description |
|
|
190
|
-
|---|---|---|
|
|
191
|
-
| **Claude Code Prompt** | on / off | Include the SDK's built-in system prompt (tool usage rules, safety guidelines, coding best practices) |
|
|
192
|
-
| **Client Prompt** | on / off | Include the system prompt sent by the connecting agent (e.g. OpenCode or Crush instructions) |
|
|
193
|
-
| **CLAUDE.md** | off / project / full | Load instruction files — `off`: none, `project`: `./CLAUDE.md` only, `full`: `~/.claude/CLAUDE.md` + `./CLAUDE.md` |
|
|
194
|
-
| **Memory** | on / off | Auto-memory: read and write memories across sessions |
|
|
195
|
-
| **Auto-Dream** | on / off | Background memory consolidation between sessions |
|
|
196
|
-
| **Thinking** | disabled / adaptive / enabled | Extended thinking mode for complex reasoning |
|
|
197
|
-
| **Thinking Passthrough** | on / off | Forward thinking blocks to the client for display |
|
|
198
|
-
| **Shared Memory** | on / off | Share memory directory with Claude Code (`~/.claude`) instead of isolated storage |
|
|
199
|
-
|
|
200
|
-
### System prompts
|
|
201
|
-
|
|
202
|
-
The system prompt controls are independent — any combination works:
|
|
203
|
-
|
|
204
|
-
- **Both enabled** (recommended): Claude Code instructions come first, followed by your agent's specific instructions. This gives Claude the full context it needs for features like memory and tool use to work correctly.
|
|
205
|
-
- **Claude Code only**: Just the base Claude Code prompt without agent-specific instructions.
|
|
206
|
-
- **Client only**: Just your agent's prompt, passed through as a raw string.
|
|
207
|
-
- **Neither**: No system prompt at all — Claude operates with just the user message.
|
|
208
|
-
|
|
209
|
-
> **Note:** For features like memory and dreaming to work well, the Claude Code system prompt should be enabled — it contains the instructions Claude needs to read and write memories correctly.
|
|
210
|
-
|
|
211
|
-
## Passthrough Mode and Tool Calling
|
|
212
|
-
|
|
213
|
-
The core question is **who executes the tools** — the SDK or the client?
|
|
214
|
-
|
|
215
|
-
- **Passthrough mode** (default for OpenCode and Pi) — Claude generates tool calls, but Meridian captures them and sends them back to the client for execution. The client runs the tool using its own implementation, with its own sandboxing, file tracking, and UI, then sends the result in the next request. This is how OpenCode, oh-my-opencagent (OMO), and most coding agents work — they have their own read/write/bash tools and need to stay in control of what runs on the user's machine.
|
|
216
|
-
- **Internal mode** — Claude Code handles everything. The SDK executes tools directly on the host, runs its full agent loop, and returns the final result. This is for clients that are purely chat interfaces (Open WebUI, simple API consumers) with no tool execution of their own.
|
|
217
|
-
|
|
218
|
-
Most users don't need to configure anything — the adapter sets the right mode automatically. To override:
|
|
219
|
-
|
|
220
|
-
```bash
|
|
221
|
-
MERIDIAN_PASSTHROUGH=1 meridian # force passthrough
|
|
222
|
-
MERIDIAN_PASSTHROUGH=0 meridian # force internal
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### How tool calling works in passthrough
|
|
226
|
-
|
|
227
|
-
1. The client sends a request with tool definitions (read, write, edit, bash, glob, grep)
|
|
228
|
-
2. Meridian registers these as MCP tools so the SDK can generate proper `tool_use` blocks
|
|
229
|
-
3. The SDK produces a tool call → Meridian captures it and returns it to the client
|
|
230
|
-
4. The client executes the tool locally and sends the result back
|
|
231
|
-
|
|
232
|
-
For large tool sets (>15 tools), non-core tools are automatically deferred via the SDK's ToolSearch mechanism. Core tools (read, write, edit, bash, glob, grep) are always loaded eagerly. The deferral threshold is configurable with `MERIDIAN_DEFER_TOOL_THRESHOLD`.
|
|
233
|
-
|
|
234
|
-
**Digest-turn elimination** — after a tool call is captured, the SDK would normally invoke the model one more time to "digest" the denial before ending the turn. That extra invocation is discarded by the proxy but fully billed — measured at ~400+ wasted output tokens and 2–3× extra latency per tool step (and on always-thinking models like Fable, a full thinking pass each time). Meridian now aborts the SDK query the moment every tool call's denial is persisted, so the digest turn never generates. Sessions remain resumable and tool-result attribution is unaffected. Kill switch: `MERIDIAN_PASSTHROUGH_EARLY_STOP=0` restores the old behavior.
|
|
235
|
-
|
|
236
|
-
### Known limitations
|
|
237
|
-
|
|
238
|
-
- **Single tool round-trip per request** — in passthrough mode, the SDK is configured with `maxTurns=3` (or 4 for deferred tools). Multi-step agentic loops where Claude needs several consecutive tool calls require the client to re-send after each round.
|
|
239
|
-
- **Blocked tools** — 10 built-in SDK tools (Read, Write, Bash, etc.) are blocked to prevent conflicts with the client's own tools. 19 additional Claude Code-only tools (CronCreate, EnterWorktree, Agent, etc.) are blocked because they require capabilities that external clients don't support.
|
|
240
|
-
- **Subagent extraction** — Meridian parses the client's Task tool description to extract subagent names and build SDK AgentDefinitions. If the client's agent framework uses a non-standard format, subagent routing may not work automatically.
|
|
241
|
-
- **Scratchpad suppression (passthrough)** — the Claude CLI advertises a proxy-host scratchpad directory that clients can't use; OpenCode 1.18+ permission-blocks writes to it. Meridian suppresses it in passthrough mode (`CLAUDE_CODE_SESSION_KIND=bg` on the subprocess). Kill switch: `MERIDIAN_SUPPRESS_SCRATCHPAD=0`.
|
|
242
|
-
- **Anthropic server tools not supported** — native server-side tools (`web_search_*`, `web_fetch_*`) are a raw Anthropic API feature (billed to an API key) that emits `server_tool_use` / `web_search_tool_result` blocks the Claude Max / Agent SDK path cannot produce. A request carrying one is rejected with a `400` explaining the fix. If a plugin needs server-side web search (e.g. [`opencode-websearch`](https://github.com/emilsvennesson/opencode-websearch)), give it its **own** provider pointed at `https://api.anthropic.com` with your `ANTHROPIC_API_KEY` — don't route that call through Meridian.
|
|
243
|
-
|
|
244
|
-
### Troubleshooting: "aborted" tool calls
|
|
245
|
-
|
|
246
|
-
Two very different things can carry the word "abort" — one is normal, one is always a bug:
|
|
247
|
-
|
|
248
|
-
- **Normal (invisible):** Meridian intentionally stops its internal SDK subprocess after your tool calls are captured — this is the optimization that avoids a wasted, billed model turn per tool call. It never appears in your client; log lines like `passthrough.early_stop` or `sdk_termination reason=aborted` in Meridian's own logs are calm, expected bookkeeping.
|
|
249
|
-
- **A bug (report it):** an **empty tool call in your client UI** — `tool {}` with "Tool execution aborted" — is never expected behavior, on any version. It means a call was cut off in transit.
|
|
250
|
-
|
|
251
|
-
**The definitive check:** the `/telemetry` dashboard's **Envelope** card. Meridian audits its own output on every response — green "wire contract clean" means every tool call was delivered intact regardless of what internal logs say. If it shows red, the logs contain `ENVELOPE VIOLATION` lines with request IDs — include those in a bug report and it can usually be root-caused directly.
|
|
252
|
-
|
|
253
|
-
## Multi-Profile Support
|
|
254
|
-
|
|
255
|
-
Meridian can route requests to different Claude accounts. Each **profile** is a named auth context — a separate Claude login with its own OAuth tokens. Switch between personal and work accounts, or share a single Meridian instance across teams.
|
|
256
|
-
|
|
257
|
-
### Adding profiles
|
|
258
|
-
|
|
259
|
-
```bash
|
|
260
|
-
# Add your personal account
|
|
261
|
-
meridian profile add personal
|
|
262
|
-
# → Opens browser for Claude login
|
|
263
|
-
|
|
264
|
-
# Add your work account (sign out of claude.ai first, then sign into the work account)
|
|
265
|
-
meridian profile add work
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
> **⚠ Important:** Claude's OAuth reuses your browser session. Before adding a second account, sign out of claude.ai and sign into the other account first.
|
|
269
|
-
|
|
270
|
-
#### Headless / SSH: complete Claude OAuth with a pasted code
|
|
271
|
-
|
|
272
|
-
When you still want a normal Claude Max browser-login profile but the Meridian host cannot open a browser (SSH, WSL, containers, remote servers), use `--headless`. Meridian prints a Claude OAuth URL, prompts for the returned code, exchanges it with PKCE, and saves the resulting credentials into the profile's isolated `CLAUDE_CONFIG_DIR`:
|
|
273
|
-
|
|
274
|
-
```bash
|
|
275
|
-
meridian profile add work --headless
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
Open the printed URL in a browser, sign in to the target Claude account, then paste the returned code at Meridian's `Paste code:` prompt. For an existing browser-login profile:
|
|
279
|
-
|
|
280
|
-
```bash
|
|
281
|
-
meridian profile login work --headless
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
#### Headless / CI: register an OAuth token
|
|
285
|
-
|
|
286
|
-
When a browser isn't available (containers, CI runners, remote shells), generate a long-lived OAuth token with `claude setup-token` and register it as a profile:
|
|
287
|
-
|
|
288
|
-
```bash
|
|
289
|
-
# Prompt for the token (input is hidden — paste the value from `claude setup-token`)
|
|
290
|
-
meridian profile add ci --oauth-token
|
|
291
|
-
|
|
292
|
-
# Or pass it inline
|
|
293
|
-
meridian profile add ci --oauth-token sk-ant-oat01-...
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
OAuth-token profiles store the token in `profiles.json` and feed it to the SDK via `CLAUDE_CODE_OAUTH_TOKEN` — no Keychain entry, no browser handshake. To prevent the SDK's 401-recovery from silently falling back to the host's `~/.claude` credentials, OAuth-token profiles also pin `CLAUDE_CONFIG_DIR` to an isolated per-profile directory under `~/.config/meridian/profiles/<name>/`. That directory holds only SDK state (sessions, settings) — never `.credentials.json`, since the token is delivered through the env.
|
|
297
|
-
|
|
298
|
-
### Switching profiles
|
|
299
|
-
|
|
300
|
-
```bash
|
|
301
|
-
# CLI (while proxy is running)
|
|
302
|
-
meridian profile switch work
|
|
303
|
-
|
|
304
|
-
# Per-request header (any agent)
|
|
305
|
-
curl -H "x-meridian-profile: work" ...
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
You can also switch profiles from the web UI — click an account card on the home page (`http://127.0.0.1:3456/`) or use the Profiles page at `/profiles`. The site header on every page shows which profile is active.
|
|
309
|
-
|
|
310
|
-
### Sticky session routing
|
|
311
|
-
|
|
312
|
-
With multiple profiles (e.g. two Claude Max subscriptions), Meridian can distribute sessions across profiles automatically while preserving **session affinity** — Anthropic's prompt caching is per-account, so a session must stay on one account to keep its ~99% cache hit rate:
|
|
313
|
-
|
|
314
|
-
```bash
|
|
315
|
-
MERIDIAN_ROUTING=sticky meridian # or set "routing": "sticky" in ~/.config/meridian/settings.json
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
- Each session is assigned to a profile by rendezvous hashing of its session id — **deterministic and stateless**, so assignments survive proxy restarts with no state to lose
|
|
319
|
-
- Adding/removing a profile only reassigns the sessions belonging to the changed arm — everything else keeps its warm cache
|
|
320
|
-
- A session's subagent/fork requests share its assignment (same session id → same account)
|
|
321
|
-
- The `x-meridian-profile` header still overrides everything, per request
|
|
322
|
-
- Default is `active` (all traffic to the active profile — the pre-existing behavior); sticky is opt-in
|
|
323
|
-
|
|
324
|
-
Request logs show the assignment (`profile=work(sticky)`), and `GET /profiles/list` reports the current `routing` mode.
|
|
325
|
-
|
|
326
|
-
### Profile commands
|
|
327
|
-
|
|
328
|
-
| Command | Description |
|
|
329
|
-
|---------|-------------|
|
|
330
|
-
| `meridian profile add <name> [--headless]` | Add a profile and authenticate via Claude OAuth; `--headless` prints a URL, prompts for the returned code, and stores the exchanged credentials |
|
|
331
|
-
| `meridian profile add <name> --oauth-token [TOKEN]` | Add a headless profile from a `claude setup-token` value (prompts when `TOKEN` is omitted) |
|
|
332
|
-
| `meridian profile list` | List profiles and auth status |
|
|
333
|
-
| `meridian profile switch <name>` | Switch the active profile (requires running proxy) |
|
|
334
|
-
| `meridian profile login <name> [--headless]` | Re-authenticate an expired profile (browser-login profiles only); `--headless` uses the URL/code flow |
|
|
335
|
-
| `meridian profile remove <name>` | Remove a profile and its credentials |
|
|
336
|
-
|
|
337
|
-
### How it works
|
|
338
|
-
|
|
339
|
-
Each profile stores its credentials in an isolated `CLAUDE_CONFIG_DIR` under `~/.config/meridian/profiles/<name>/`. OAuth-token profiles use the same isolated directory layout — but the token itself lives in `~/.config/meridian/profiles.json` and is fed to the SDK via `CLAUDE_CODE_OAUTH_TOKEN`, so the per-profile dir holds only SDK state (sessions, settings) and never the credential. When a request arrives, Meridian resolves the profile in priority order:
|
|
340
|
-
|
|
341
|
-
1. `x-meridian-profile` request header (per-request override)
|
|
342
|
-
2. Active profile (set via `meridian profile switch` or the web UI)
|
|
343
|
-
3. First configured profile
|
|
344
|
-
|
|
345
|
-
Session state is scoped per profile — switching accounts won't cross-contaminate conversation history.
|
|
346
|
-
|
|
347
|
-
### Environment variable configuration
|
|
348
|
-
|
|
349
|
-
For advanced setups (CI, Docker), profiles can also be provided via environment variable:
|
|
350
|
-
|
|
351
|
-
```bash
|
|
352
|
-
export MERIDIAN_PROFILES='[
|
|
353
|
-
{"id":"personal","claudeConfigDir":"/path/to/config1"},
|
|
354
|
-
{"id":"work","claudeConfigDir":"/path/to/config2"},
|
|
355
|
-
{"id":"ci","oauthToken":"sk-ant-oat01-..."}
|
|
356
|
-
]'
|
|
357
|
-
export MERIDIAN_DEFAULT_PROFILE=personal
|
|
358
|
-
meridian
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
Profile shapes:
|
|
362
|
-
|
|
363
|
-
- `claudeConfigDir` — points at a `~/.claude`-style directory; uses Claude Max OAuth from that dir
|
|
364
|
-
- `apiKey` (with optional `baseUrl`) — direct Anthropic API access; sets `ANTHROPIC_API_KEY` / `ANTHROPIC_BASE_URL`
|
|
365
|
-
- `oauthToken` — long-lived token from `claude setup-token`; sets `CLAUDE_CODE_OAUTH_TOKEN`, no config dir needed
|
|
366
|
-
|
|
367
|
-
When `MERIDIAN_PROFILES` is set, it takes precedence over disk-configured profiles. When unset, Meridian auto-discovers profiles from `~/.config/meridian/profiles.json` on each request.
|
|
368
|
-
|
|
369
|
-
Related environment variables:
|
|
370
|
-
|
|
371
|
-
- `MERIDIAN_ROUTING=sticky` — enable [sticky session routing](#sticky-session-routing) across profiles (default `active`)
|
|
372
|
-
- `MERIDIAN_ADAPTER_INSTANCES='{...}'` — define [adapter instances](#adapter-instances) inline instead of via `~/.config/meridian/adapter-instances.json`
|
|
373
|
-
|
|
374
|
-
## Agent Setup
|
|
375
|
-
|
|
376
|
-
### OpenCode
|
|
377
|
-
|
|
378
|
-
**Step 1: Run `meridian setup` (required, one time)**
|
|
379
|
-
|
|
380
|
-
```bash
|
|
381
|
-
meridian setup
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
This adds the Meridian plugin to your OpenCode global config (`~/.config/opencode/opencode.json`). The plugin enables:
|
|
385
|
-
|
|
386
|
-
- **Session tracking** — reliable conversation continuity across requests
|
|
387
|
-
- **Safe model defaults** — Opus uses 1M context (included with Max subscription); Sonnet uses 200k to avoid Extra Usage charges ([details](#configuration))
|
|
388
|
-
- **Subagent model selection** — subagents automatically use `sonnet`/`opus` (200k), preserving rate-limit budget
|
|
389
|
-
|
|
390
|
-
If the plugin is missing, Meridian warns at startup and reports `"plugin": "not-configured"` in the health endpoint.
|
|
391
|
-
|
|
392
|
-
**Step 2: Start**
|
|
393
|
-
|
|
394
|
-
```bash
|
|
395
|
-
ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://127.0.0.1:3456 opencode
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
Or set these in your shell profile so they're always active:
|
|
399
|
-
|
|
400
|
-
```bash
|
|
401
|
-
export ANTHROPIC_API_KEY=x
|
|
402
|
-
export ANTHROPIC_BASE_URL=http://127.0.0.1:3456
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
#### oh-my-opencagent (OMO)
|
|
406
|
-
|
|
407
|
-
[oh-my-opencagent](https://github.com/nicobailey/oh-my-opencagent) adds multi-agent orchestration on top of OpenCode. It works transparently through Meridian with no extra configuration — OMO uses the same OpenCode headers and tool format, so Meridian detects it automatically.
|
|
408
|
-
|
|
409
|
-
Meridian parses OMO's Task tool descriptions to extract subagent names (explore, code-review, etc.) and builds SDK AgentDefinitions so Claude can route to the correct agent. Internal orchestration markers (`<!-- OMO_INTERNAL_INITIATOR -->`, `[SYSTEM DIRECTIVE: OH-MY-OPENCODE ...]`) are stripped automatically to prevent context leakage.
|
|
410
|
-
|
|
411
|
-
OMO requires **passthrough mode** (the default for OpenCode) — subagent delegation flows through tool calls that must be forwarded back to the client.
|
|
412
|
-
|
|
413
|
-
### Crush
|
|
414
|
-
|
|
415
|
-
Add a provider to `~/.config/crush/crush.json`:
|
|
416
|
-
|
|
417
|
-
```json
|
|
418
|
-
{
|
|
419
|
-
"providers": {
|
|
420
|
-
"meridian": {
|
|
421
|
-
"id": "meridian",
|
|
422
|
-
"name": "Meridian",
|
|
423
|
-
"type": "anthropic",
|
|
424
|
-
"base_url": "http://127.0.0.1:3456",
|
|
425
|
-
"api_key": "dummy",
|
|
426
|
-
"models": [
|
|
427
|
-
{ "id": "claude-fable-5", "name": "Claude Fable 5 (1M)", "context_window": 1000000, "default_max_tokens": 32768, "can_reason": true, "supports_attachments": true },
|
|
428
|
-
{ "id": "claude-opus-4-8", "name": "Claude Opus 4.8 (1M)", "context_window": 1000000, "default_max_tokens": 32768, "can_reason": true, "supports_attachments": true },
|
|
429
|
-
{ "id": "claude-opus-4-7", "name": "Claude Opus 4.7 (1M)", "context_window": 1000000, "default_max_tokens": 32768, "can_reason": true, "supports_attachments": true },
|
|
430
|
-
{ "id": "claude-sonnet-4-6", "name": "Claude Sonnet 4.6 (1M)", "context_window": 1000000, "default_max_tokens": 64000, "can_reason": true, "supports_attachments": true },
|
|
431
|
-
{ "id": "claude-opus-4-6", "name": "Claude Opus 4.6 (1M)", "context_window": 1000000, "default_max_tokens": 32768, "can_reason": true, "supports_attachments": true },
|
|
432
|
-
{ "id": "claude-haiku-4-5-20251001", "name": "Claude Haiku 4.5", "context_window": 200000, "default_max_tokens": 16384, "can_reason": true, "supports_attachments": true }
|
|
433
|
-
]
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
```bash
|
|
440
|
-
crush run --model meridian/claude-sonnet-4-6 "refactor this function"
|
|
441
|
-
crush --model meridian/claude-opus-4-6 # interactive TUI
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
Crush is automatically detected from its `Charm-Crush/` User-Agent — no plugin needed.
|
|
445
|
-
|
|
446
|
-
### Droid (Factory AI)
|
|
447
|
-
|
|
448
|
-
Add Meridian as a custom model provider in `~/.factory/settings.json`:
|
|
449
|
-
|
|
450
|
-
```json
|
|
451
|
-
{
|
|
452
|
-
"customModels": [
|
|
453
|
-
{ "model": "claude-fable-5", "name": "Fable 5 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
|
|
454
|
-
{ "model": "claude-opus-4-8", "name": "Opus 4.8 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
|
|
455
|
-
{ "model": "claude-opus-4-7", "name": "Opus 4.7 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
|
|
456
|
-
{ "model": "claude-sonnet-4-6", "name": "Sonnet 4.6 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
|
|
457
|
-
{ "model": "claude-opus-4-6", "name": "Opus 4.6 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
|
|
458
|
-
{ "model": "claude-haiku-4-5-20251001", "name": "Haiku 4.5 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" }
|
|
459
|
-
]
|
|
460
|
-
}
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
Then pick any `custom:claude-*` model in the Droid TUI. No plugin needed — Droid is automatically detected.
|
|
464
|
-
|
|
465
|
-
### Cline
|
|
466
|
-
|
|
467
|
-
**1. Authenticate:**
|
|
468
|
-
|
|
469
|
-
```bash
|
|
470
|
-
cline auth --provider anthropic --apikey "dummy" --modelid "claude-sonnet-4-6"
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
**2. Set the proxy URL** in `~/.cline/data/globalState.json`:
|
|
474
|
-
|
|
475
|
-
```json
|
|
476
|
-
{
|
|
477
|
-
"anthropicBaseUrl": "http://127.0.0.1:3456",
|
|
478
|
-
"actModeApiProvider": "anthropic",
|
|
479
|
-
"actModeApiModelId": "claude-sonnet-4-6"
|
|
480
|
-
}
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
**3. Run:**
|
|
484
|
-
|
|
485
|
-
```bash
|
|
486
|
-
cline --yolo "refactor the login function"
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
No plugin needed — Cline uses the standard Anthropic SDK.
|
|
490
|
-
|
|
491
|
-
### Aider
|
|
492
|
-
|
|
493
|
-
```bash
|
|
494
|
-
ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://127.0.0.1:3456 \
|
|
495
|
-
aider --model anthropic/claude-sonnet-4-6
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
> **Note:** `--no-stream` is incompatible due to a litellm parsing issue — use the default streaming mode.
|
|
499
|
-
|
|
500
|
-
### Codex CLI
|
|
501
|
-
|
|
502
|
-
Codex CLI ≥ 0.96 dropped `wire_api = "chat"` and speaks only the OpenAI **Responses API** (`/v1/responses`), which Meridian serves. Add a provider to `~/.codex/config.toml`:
|
|
503
|
-
|
|
504
|
-
```toml
|
|
505
|
-
model = "claude-sonnet-5"
|
|
506
|
-
model_provider = "meridian"
|
|
507
|
-
|
|
508
|
-
[model_providers.meridian]
|
|
509
|
-
name = "Meridian"
|
|
510
|
-
base_url = "http://127.0.0.1:3456/v1"
|
|
511
|
-
wire_api = "responses"
|
|
512
|
-
env_key = "MERIDIAN_KEY" # any value unless MERIDIAN_API_KEY is set
|
|
513
|
-
```
|
|
514
|
-
|
|
515
|
-
```bash
|
|
516
|
-
MERIDIAN_KEY=x codex "refactor this function"
|
|
517
|
-
MERIDIAN_KEY=x codex exec "run the tests and summarize failures" # non-interactive
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
Codex is a tool-driving agent — Meridian runs the `/v1/responses` endpoint in **passthrough** mode automatically (Codex executes its own shell/apply-patch tools), so no `MERIDIAN_PASSTHROUGH` change is needed. A harmless `Model metadata for 'claude-sonnet-5' not found` warning from Codex is expected — it doesn't recognize non-OpenAI model ids but works regardless.
|
|
521
|
-
|
|
522
|
-
`model_reasoning_effort` is supported and won't stall the CLI, but Claude's private thinking isn't yet carried **across** turns — the Responses API's encrypted-reasoning envelope is OpenAI-specific and incompatible with Claude's signed thinking blocks, so cross-turn reasoning continuity is deferred (each turn still reasons with full context including tool results). Verified on Codex 0.144 with plain, tool-driving, and reasoning-enabled turns.
|
|
523
|
-
|
|
524
|
-
### OpenAI-compatible tools (Open WebUI, Continue, etc.)
|
|
525
|
-
|
|
526
|
-
Meridian speaks the OpenAI protocol natively — no LiteLLM or translation proxy needed.
|
|
527
|
-
|
|
528
|
-
**`POST /v1/chat/completions`** — accepts OpenAI chat format, returns OpenAI completion format (streaming and non-streaming)
|
|
529
|
-
|
|
530
|
-
- `image_url` parts are supported when provided as **data URLs** (`data:image/...;base64,...`)
|
|
531
|
-
- multimodal tool flows where a tool returns `tool_result.content = [text, image]` are preserved through the structured multimodal path instead of being flattened to text
|
|
532
|
-
|
|
533
|
-
**`GET /v1/models`** — returns available Claude models in OpenAI format
|
|
534
|
-
|
|
535
|
-
Point any OpenAI-compatible tool at `http://127.0.0.1:3456` with any API key value:
|
|
536
|
-
|
|
537
|
-
```bash
|
|
538
|
-
# Open WebUI: set OpenAI API base to http://127.0.0.1:3456, API key to any value
|
|
539
|
-
# Continue: set apiBase to http://127.0.0.1:3456 with provider: openai
|
|
540
|
-
# Any OpenAI SDK: set base_url="http://127.0.0.1:3456", api_key="dummy"
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
> **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.
|
|
544
|
-
|
|
545
|
-
### Cherry Studio
|
|
546
|
-
|
|
547
|
-
[Cherry Studio](https://github.com/CherryHQ/cherry-studio) is a desktop chat client. Point it at Meridian by setting the Anthropic API base URL to `http://127.0.0.1:3456` (any API key value works).
|
|
548
|
-
|
|
549
|
-
Because Cherry Studio is a chat client rather than a coding agent, select the `cherry` adapter so Claude's **built-in web search** is available (coding-agent adapters block it in favour of their own):
|
|
550
|
-
|
|
551
|
-
```bash
|
|
552
|
-
MERIDIAN_DEFAULT_AGENT=cherry meridian
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
The `cherry` adapter runs in internal mode: Claude executes `WebSearch`/`WebFetch` itself and Meridian returns the grounded answer — the internal tool calls are hidden from the client. This resolves the "no WebSearch/WebFetch tool exposed" error (#481).
|
|
556
|
-
|
|
557
|
-
> Cherry Studio doesn't send a Meridian-specific header, so set `MERIDIAN_DEFAULT_AGENT=cherry` on a Meridian dedicated to it, or send `x-meridian-agent: cherry` if your setup allows custom headers.
|
|
558
|
-
|
|
559
|
-
### ForgeCode
|
|
560
|
-
|
|
561
|
-
Add a custom provider to `~/forge/.forge.toml`:
|
|
562
|
-
|
|
563
|
-
```toml
|
|
564
|
-
[[providers]]
|
|
565
|
-
id = "meridian"
|
|
566
|
-
url = "http://127.0.0.1:3456/v1/messages"
|
|
567
|
-
models = "http://127.0.0.1:3456/v1/models"
|
|
568
|
-
api_key_vars = "MERIDIAN_FORGE_KEY"
|
|
569
|
-
response_type = "Anthropic"
|
|
570
|
-
auth_methods = ["api_key"]
|
|
571
|
-
|
|
572
|
-
[session]
|
|
573
|
-
provider_id = "meridian"
|
|
574
|
-
model_id = "claude-opus-4-6"
|
|
575
|
-
```
|
|
576
|
-
|
|
577
|
-
Set the API key env var. Any value works unless you've enabled authentication with `MERIDIAN_API_KEY`, in which case use your auth key here:
|
|
578
|
-
|
|
579
|
-
```bash
|
|
580
|
-
export MERIDIAN_FORGE_KEY=x
|
|
581
|
-
```
|
|
582
|
-
|
|
583
|
-
Then log in and select the model:
|
|
584
|
-
|
|
585
|
-
```bash
|
|
586
|
-
forge provider login meridian # enter any value when prompted
|
|
587
|
-
forge config set provider meridian --model claude-opus-4-6
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
Start Meridian with the ForgeCode adapter:
|
|
591
|
-
|
|
592
|
-
```bash
|
|
593
|
-
MERIDIAN_DEFAULT_AGENT=forgecode meridian
|
|
594
|
-
```
|
|
595
|
-
|
|
596
|
-
ForgeCode uses reqwest's default User-Agent, so automatic detection isn't possible. The `MERIDIAN_DEFAULT_AGENT` env var tells Meridian to use the ForgeCode adapter for all unrecognized requests. If you run other agents alongside ForgeCode, use the `x-meridian-agent: forgecode` header instead (add `[providers.headers]` to your `.forge.toml`).
|
|
597
|
-
|
|
598
|
-
### Pi
|
|
599
|
-
|
|
600
|
-
Pi uses the `@mariozechner/pi-ai` library which supports a configurable `baseUrl` on the model. Add a provider-level override in `~/.pi/agent/models.json`:
|
|
601
|
-
|
|
602
|
-
```json
|
|
603
|
-
{
|
|
604
|
-
"providers": {
|
|
605
|
-
"anthropic": {
|
|
606
|
-
"baseUrl": "http://127.0.0.1:3456",
|
|
607
|
-
"apiKey": "x",
|
|
608
|
-
"headers": {
|
|
609
|
-
"x-meridian-agent": "pi"
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
```
|
|
615
|
-
|
|
616
|
-
Pi mimics Claude Code's User-Agent, so automatic detection isn't possible. The `x-meridian-agent: pi` header in the config above tells Meridian to use the Pi adapter. Alternatively, if Pi is your only agent, you can set `MERIDIAN_DEFAULT_AGENT=pi` as an env var instead.
|
|
617
|
-
|
|
618
|
-
Pi runs in passthrough mode by default — it executes its own tools and Meridian just forwards the `tool_use` blocks. Opt out with `MERIDIAN_PASSTHROUGH=0`.
|
|
619
|
-
|
|
620
|
-
### Claude Code
|
|
621
|
-
|
|
622
|
-
Claude Code can point at Meridian like any other Anthropic API client. The
|
|
623
|
-
common use case is sharing a single Claude Max subscription from one host
|
|
624
|
-
across other machines on your network — run Meridian on the box that is
|
|
625
|
-
logged into Claude Max, then run Claude Code anywhere else against it.
|
|
626
|
-
|
|
627
|
-
```bash
|
|
628
|
-
# On another machine (or the same one)
|
|
629
|
-
ANTHROPIC_AUTH_TOKEN=x ANTHROPIC_BASE_URL=http://meridian-host:3456 claude
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
> **Note:** Use `ANTHROPIC_AUTH_TOKEN` (or `ANTHROPIC_API_KEY`) — Claude Code
|
|
633
|
-
> treats both as bearer credentials. Set the value to your `MERIDIAN_API_KEY`
|
|
634
|
-
> if you've enabled authentication, otherwise any string works.
|
|
635
|
-
|
|
636
|
-
> ⚠️ **Security for multi-machine setups.** If you expose Meridian beyond
|
|
637
|
-
> loopback (e.g. bind to `0.0.0.0` or a LAN IP), **set `MERIDIAN_API_KEY` to a
|
|
638
|
-
> strong secret** and require it on clients. An unprotected network-accessible
|
|
639
|
-
> proxy is a Claude Max credential leak — anyone who can reach the port can
|
|
640
|
-
> burn your subscription.
|
|
641
|
-
|
|
642
|
-
Claude Code is detected automatically via its `claude-cli/*` User-Agent.
|
|
643
|
-
Requests flow through the Claude Code adapter which:
|
|
644
|
-
|
|
645
|
-
- Parses the client's real working directory from its `Primary working directory:` system-prompt line so Claude answers path-related questions with your local path, not the proxy host's.
|
|
646
|
-
- Leaves the SDK subprocess cwd on the proxy host (Claude Code's local paths don't exist there).
|
|
647
|
-
- Runs in passthrough mode by default — Claude Code executes its own tools on the machine it runs on; Meridian just forwards tool_use blocks.
|
|
648
|
-
|
|
649
|
-
### Adapter instances
|
|
650
|
-
|
|
651
|
-
Run several configurations of the same adapter side by side — e.g. a passthrough variant with thinking enabled and one without, or a dedicated config for a specific client. Define instances in `~/.config/meridian/adapter-instances.json` (or the `MERIDIAN_ADAPTER_INSTANCES` env var as a JSON string):
|
|
652
|
-
|
|
653
|
-
```jsonc
|
|
654
|
-
{
|
|
655
|
-
"oc-thinky": { "base": "opencode", "features": { "thinking": "enabled" } },
|
|
656
|
-
"lite-plain": { "base": "passthrough", "passthrough": true,
|
|
657
|
-
"match": { "userAgentPrefix": "litellm/" } },
|
|
658
|
-
"team-webui": { "base": "opencode", "features": { "codeSystemPrompt": false },
|
|
659
|
-
"match": { "header": { "x-team": "alpha" } } }
|
|
660
|
-
}
|
|
661
|
-
```
|
|
662
|
-
|
|
663
|
-
- **`base`** — which built-in adapter provides the behavior (tool handling, session tracking, transforms). Existing plugins and transforms scoped to the base adapter apply to its instances automatically.
|
|
664
|
-
- **`features`** — per-instance overrides of the [SDK feature toggles](#sdk-feature-toggles-experimental) (thinking, system prompts, memory, ...) layered over the base's settings. Same keys as the settings UI.
|
|
665
|
-
- **`passthrough`** — per-instance passthrough mode, overriding the adapter default and `MERIDIAN_PASSTHROUGH`.
|
|
666
|
-
- **`match`** — optional automatic selection: exact header values and/or a User-Agent prefix. Match rules outrank built-in User-Agent detection (that's their purpose). Without `match`, select the instance per request with `x-meridian-agent: <instance-name>`.
|
|
667
|
-
|
|
668
|
-
Built-in adapter names are reserved and can't be shadowed. With no instances configured, detection is exactly the built-in chain. Config file changes apply within ~5s, no restart needed.
|
|
669
|
-
|
|
670
|
-
### Claude Design MCP
|
|
671
|
-
|
|
672
|
-
Meridian proxies the Claude Design MCP API (`api.anthropic.com/v1/design/*`), so MCP clients can use Claude Design tools through your local endpoint.
|
|
673
|
-
|
|
674
|
-
**1. Add the MCP server.** For Claude Code:
|
|
675
|
-
|
|
676
|
-
```bash
|
|
677
|
-
claude mcp add -s user --transport http claude-design http://127.0.0.1:3456/v1/design/mcp
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
Any other MCP client: point it at `http://127.0.0.1:3456/v1/design/mcp` (streamable HTTP).
|
|
681
|
-
|
|
682
|
-
**2. Grant Claude Design consent (one time, per Anthropic account).** Tool calls return a `needs_consent` error until you enable it: open [claude.ai/design/settings](https://claude.ai/design/settings), find **"Claude product access"** ("Let other Claude products, like Claude Code, read and edit your Design projects"), and switch it **On**. This is a setting on the Anthropic account itself — with multiple Meridian profiles, the account behind the *profile handling the request* is the one that needs the toggle.
|
|
683
|
-
|
|
684
|
-
That's it — your existing Claude Max login covers auth (`initialize`, `tools/list`, and tool calls are all verified working with a plain Max token). Verify with a quick handshake:
|
|
685
|
-
|
|
686
|
-
```bash
|
|
687
|
-
curl -s -X POST http://127.0.0.1:3456/v1/design/mcp -H 'content-type: application/json' \
|
|
688
|
-
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
|
|
689
|
-
```
|
|
690
|
-
|
|
691
|
-
**Multiple profiles:** design requests use the active profile by default. To pin design traffic to a specific profile regardless of which is active, register the server with a profile header:
|
|
692
|
-
|
|
693
|
-
```bash
|
|
694
|
-
claude mcp add -s user --transport http --header "x-meridian-profile: personal" \
|
|
695
|
-
claude-design http://127.0.0.1:3456/v1/design/mcp
|
|
696
|
-
```
|
|
697
|
-
|
|
698
|
-
**Fallback OAuth flow:** if the upstream ever rejects your token with an `auth_error` (scope enforcement has varied over time), `/design-login` obtains a dedicated token with the `user:design:read`/`user:design:write` scopes:
|
|
699
|
-
|
|
700
|
-
```bash
|
|
701
|
-
curl http://127.0.0.1:3456/design-login # returns an authorize URL — open it in your browser
|
|
702
|
-
curl -X POST http://127.0.0.1:3456/design-login \
|
|
703
|
-
-H 'content-type: application/json' \
|
|
704
|
-
-d '{"code": "<code-from-browser>"}' # paste the code you were shown
|
|
705
|
-
```
|
|
706
|
-
|
|
707
|
-
The design token is stored at `~/.config/meridian/design-token.json` (mode `0600`, global across profiles) and refreshed automatically when it expires.
|
|
708
|
-
|
|
709
|
-
> Contributed by [@sittitep](https://github.com/sittitep) (#543).
|
|
710
|
-
|
|
711
|
-
### Any Anthropic-compatible tool
|
|
712
|
-
|
|
713
|
-
```bash
|
|
714
|
-
export ANTHROPIC_API_KEY=x
|
|
715
|
-
export ANTHROPIC_BASE_URL=http://127.0.0.1:3456
|
|
716
|
-
```
|
|
717
|
-
|
|
718
95
|
## Tested Agents
|
|
719
96
|
|
|
720
97
|
| Agent | Status | Notes |
|
|
721
98
|
|-------|--------|-------|
|
|
722
|
-
| [OpenCode](https://github.com/anomalyco/opencode) | ✅ Verified | Requires `meridian setup` — full tool support, session resume, streaming, subagents |
|
|
723
|
-
| [ForgeCode](https://forgecode.dev) | ✅ Verified | Provider config (see
|
|
724
|
-
| [Droid (Factory AI)](https://factory.ai/product/ide) | ✅ Verified | BYOK config (see
|
|
725
|
-
| [Crush](https://github.com/charmbracelet/crush) | ✅ Verified | Provider config (see
|
|
726
|
-
| [Cline](https://github.com/cline/cline) | ✅ Verified | Config (see
|
|
99
|
+
| [OpenCode](https://github.com/anomalyco/opencode) | ✅ Verified | Requires `meridian setup` ([setup](docs/agents.md#opencode)) — full tool support, session resume, streaming, subagents |
|
|
100
|
+
| [ForgeCode](https://forgecode.dev) | ✅ Verified | Provider config (see [Agent Setup](docs/agents.md)) — passthrough tool execution, session resume, streaming |
|
|
101
|
+
| [Droid (Factory AI)](https://factory.ai/product/ide) | ✅ Verified | BYOK config (see [Agent Setup](docs/agents.md)) — full tool support, session resume, streaming |
|
|
102
|
+
| [Crush](https://github.com/charmbracelet/crush) | ✅ Verified | Provider config (see [Agent Setup](docs/agents.md)) — full tool support, session resume, headless `crush run` |
|
|
103
|
+
| [Cline](https://github.com/cline/cline) | ✅ Verified | Config (see [Agent Setup](docs/agents.md)) — full tool support, file read/write/edit, bash, session resume |
|
|
727
104
|
| [Aider](https://github.com/paul-gauthier/aider) | ✅ Verified | Env vars — file editing, streaming; `--no-stream` broken (litellm bug) |
|
|
728
105
|
| [Open WebUI](https://github.com/open-webui/open-webui) | ✅ Verified | OpenAI-compatible endpoints — set base URL to `http://127.0.0.1:3456` |
|
|
729
|
-
| [Pi](https://github.com/mariozechner/pi-coding-agent) | ✅ Verified | models.json config (see
|
|
106
|
+
| [Pi](https://github.com/mariozechner/pi-coding-agent) | ✅ Verified | models.json config (see [Agent Setup](docs/agents.md)) — full tool support via passthrough; detected via `x-meridian-agent: pi` header |
|
|
730
107
|
| [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | ✅ Verified | `ANTHROPIC_BASE_URL` — remote clients share a Max subscription over the network; client CWD preserved in system prompt |
|
|
731
|
-
| [Cherry Studio](https://github.com/CherryHQ/cherry-studio) | ✅ Verified | `cherry` adapter (see
|
|
732
|
-
| [Codex CLI](https://github.com/openai/codex) | ✅ Verified | `/v1/responses` (see
|
|
108
|
+
| [Cherry Studio](https://github.com/CherryHQ/cherry-studio) | ✅ Verified | `cherry` adapter (see [Agent Setup](docs/agents.md)) — chat client with Claude's built-in web search via internal mode |
|
|
109
|
+
| [Codex CLI](https://github.com/openai/codex) | ✅ Verified | `/v1/responses` (see [Agent Setup](docs/agents.md)) — Responses-API provider, passthrough tool execution; verified on 0.144 (plain + tool-driving turns) |
|
|
733
110
|
| [Continue](https://github.com/continuedev/continue) | 🔲 Untested | OpenAI-compatible endpoints should work — set `apiBase` to `http://127.0.0.1:3456` |
|
|
734
111
|
|
|
735
112
|
Tested an agent or built a plugin? [Open an issue](https://github.com/rynfar/meridian/issues) and we'll add it.
|
|
736
113
|
|
|
737
|
-
## Architecture
|
|
738
|
-
|
|
739
|
-
```
|
|
740
|
-
src/proxy/
|
|
741
|
-
├── server.ts ← HTTP orchestration (routes, SSE streaming, concurrency)
|
|
742
|
-
├── adapter.ts ← AgentAdapter interface
|
|
743
|
-
├── adapters/
|
|
744
|
-
│ ├── detect.ts ← Agent detection from request headers
|
|
745
|
-
│ ├── opencode.ts ← OpenCode adapter
|
|
746
|
-
│ ├── forgecode.ts ← ForgeCode adapter
|
|
747
|
-
│ ├── crush.ts ← Crush adapter
|
|
748
|
-
│ ├── droid.ts ← Droid adapter
|
|
749
|
-
│ ├── pi.ts ← Pi adapter
|
|
750
|
-
│ ├── cherry.ts ← Cherry Studio adapter (internal mode + web search)
|
|
751
|
-
│ ├── claudecode.ts ← Claude Code adapter (remote clients sharing a Max host)
|
|
752
|
-
│ ├── openai.ts ← OpenAI-endpoint adapter (/v1/chat/completions)
|
|
753
|
-
│ ├── codex.ts ← Codex CLI adapter (/v1/responses, forced passthrough)
|
|
754
|
-
│ └── passthrough.ts ← LiteLLM passthrough adapter
|
|
755
|
-
├── query.ts ← SDK query options builder
|
|
756
|
-
├── errors.ts ← Error classification
|
|
757
|
-
├── models.ts ← Model mapping (sonnet/opus/haiku, agentMode)
|
|
758
|
-
├── tokenRefresh.ts ← Cross-platform OAuth token refresh
|
|
759
|
-
├── openai.ts ← OpenAI ↔ Anthropic format translation (pure)
|
|
760
|
-
├── openaiResponses.ts ← OpenAI Responses API ↔ Anthropic translation (pure)
|
|
761
|
-
├── setup.ts ← OpenCode plugin configuration
|
|
762
|
-
├── session/
|
|
763
|
-
│ ├── lineage.ts ← Per-message hashing, mutation classification (pure)
|
|
764
|
-
│ ├── fingerprint.ts ← Conversation fingerprinting
|
|
765
|
-
│ └── cache.ts ← LRU session caches
|
|
766
|
-
├── profiles.ts ← Multi-profile: resolve, list, switch auth contexts
|
|
767
|
-
├── profileCli.ts ← CLI commands for profile management
|
|
768
|
-
├── sessionStore.ts ← Cross-proxy file-based session persistence
|
|
769
|
-
└── passthroughTools.ts ← Tool forwarding mode
|
|
770
|
-
telemetry/
|
|
771
|
-
├── ...
|
|
772
|
-
├── profileBar.ts ← Shared site header (brand, nav, status, active profile)
|
|
773
|
-
└── profilePage.ts ← Profile management page
|
|
774
|
-
plugin/
|
|
775
|
-
└── meridian.ts ← OpenCode plugin (session headers + agent mode)
|
|
776
|
-
```
|
|
777
|
-
|
|
778
|
-
### Session Management
|
|
779
|
-
|
|
780
|
-
Every incoming request is classified:
|
|
781
|
-
|
|
782
|
-
| Classification | What Happened | Action |
|
|
783
|
-
|---------------|---------------|--------|
|
|
784
|
-
| **Continuation** | New messages appended | Resume SDK session |
|
|
785
|
-
| **Compaction** | Agent summarized old messages | Resume (suffix preserved) |
|
|
786
|
-
| **Undo** | User rolled back messages | Fork at rollback point |
|
|
787
|
-
| **Diverged** | Completely different conversation | Start fresh |
|
|
788
|
-
|
|
789
|
-
Sessions are stored in-memory (LRU) and persisted to `~/.cache/meridian/sessions.json` for cross-proxy resume.
|
|
790
|
-
|
|
791
|
-
### Agent Detection
|
|
792
|
-
|
|
793
|
-
Agents are identified from request headers automatically:
|
|
794
|
-
|
|
795
|
-
| Signal | Adapter |
|
|
796
|
-
|---|---|
|
|
797
|
-
| `x-meridian-agent` header | Explicit override (any adapter) |
|
|
798
|
-
| `x-opencode-session` or `x-session-affinity` header | OpenCode |
|
|
799
|
-
| `opencode/` User-Agent | OpenCode |
|
|
800
|
-
| `factory-cli/` User-Agent | Droid |
|
|
801
|
-
| `Charm-Crush/` User-Agent | Crush |
|
|
802
|
-
| `claude-cli/` User-Agent | Claude Code (unless `MERIDIAN_DEFAULT_AGENT` overrides — Pi mimics this UA) |
|
|
803
|
-
| `litellm/` UA or `x-litellm-*` headers | LiteLLM passthrough |
|
|
804
|
-
| *(anything else)* | `MERIDIAN_DEFAULT_AGENT` env var, or OpenCode |
|
|
805
|
-
|
|
806
|
-
### Adding a New Agent
|
|
807
|
-
|
|
808
|
-
Implement the `AgentAdapter` interface in `src/proxy/adapters/`. See [`adapters/opencode.ts`](src/proxy/adapters/opencode.ts) for a reference.
|
|
809
|
-
|
|
810
|
-
## API Key Authentication
|
|
811
|
-
|
|
812
|
-
By default, Meridian binds to `127.0.0.1` and requires no authentication — anyone on localhost can use it. If you expose Meridian over a network (Tailscale, LAN, Docker with port mapping), you can enable API key authentication to prevent unauthorized access.
|
|
813
|
-
|
|
814
|
-
```bash
|
|
815
|
-
MERIDIAN_API_KEY=your-secret-key meridian
|
|
816
|
-
```
|
|
817
|
-
|
|
818
|
-
When set:
|
|
819
|
-
- All API routes (`/v1/messages`, `/v1/chat/completions`, etc.) and admin routes (`/telemetry`, `/metrics`, `/profiles`) require a matching key
|
|
820
|
-
- `/` and `/health` remain open (monitoring tools need unauthenticated health checks)
|
|
821
|
-
- Keys are accepted via `x-api-key` header or `Authorization: Bearer` header
|
|
822
|
-
|
|
823
|
-
Clients just set their `ANTHROPIC_API_KEY` to the shared secret — since most tools already send this header, no workflow changes are needed:
|
|
824
|
-
|
|
825
|
-
```bash
|
|
826
|
-
ANTHROPIC_API_KEY=your-secret-key ANTHROPIC_BASE_URL=http://meridian-host:3456 opencode
|
|
827
|
-
```
|
|
828
|
-
|
|
829
|
-
## Configuration
|
|
830
|
-
|
|
831
|
-
| Variable | Alias | Default | Description |
|
|
832
|
-
|----------|-------|---------|-------------|
|
|
833
|
-
| `MERIDIAN_API_KEY` | — | unset | Shared secret for API key authentication. When set, all API and admin routes require a matching `x-api-key` or `Authorization: Bearer` header. `/` and `/health` remain open. |
|
|
834
|
-
| `MERIDIAN_PORT` | `CLAUDE_PROXY_PORT` | `3456` | Port to listen on |
|
|
835
|
-
| `MERIDIAN_HOST` | `CLAUDE_PROXY_HOST` | `127.0.0.1` | Host to bind to |
|
|
836
|
-
| `MERIDIAN_PASSTHROUGH` | `CLAUDE_PROXY_PASSTHROUGH` | unset | Forward tool calls to client instead of executing |
|
|
837
|
-
| `MERIDIAN_MAX_CONCURRENT` | `CLAUDE_PROXY_MAX_CONCURRENT` | `10` | Maximum concurrent SDK sessions |
|
|
838
|
-
| `MERIDIAN_MAX_SESSIONS` | `CLAUDE_PROXY_MAX_SESSIONS` | `1000` | In-memory LRU session cache size |
|
|
839
|
-
| `MERIDIAN_MAX_STORED_SESSIONS` | `CLAUDE_PROXY_MAX_STORED_SESSIONS` | `10000` | File-based session store capacity |
|
|
840
|
-
| `MERIDIAN_WORKDIR` | `CLAUDE_PROXY_WORKDIR` | `cwd()` | Default working directory for SDK |
|
|
841
|
-
| `MERIDIAN_IDLE_TIMEOUT_SECONDS` | `CLAUDE_PROXY_IDLE_TIMEOUT_SECONDS` | `120` | HTTP keep-alive timeout |
|
|
842
|
-
| `MERIDIAN_TELEMETRY_SIZE` | `CLAUDE_PROXY_TELEMETRY_SIZE` | `1000` | Telemetry ring buffer size |
|
|
843
|
-
| `MERIDIAN_NO_FILE_CHANGES` | `CLAUDE_PROXY_NO_FILE_CHANGES` | unset | Disable "Files changed" summary in responses |
|
|
844
|
-
| `MERIDIAN_SONNET_MODEL` | `CLAUDE_PROXY_SONNET_MODEL` | `sonnet` | Sonnet context tier: `sonnet` (200k, default) or `sonnet[1m]` (1M, requires Extra Usage†) |
|
|
845
|
-
| `MERIDIAN_1M_CONTEXT_SUPPORT` | `CLAUDE_PROXY_1M_CONTEXT_SUPPORT` | unset | Set to `0`/`false`/`no` to disable 1M context entirely — every model resolves to its 200k base variant, so Meridian never requests the extended window (avoids Extra Usage on 1M). |
|
|
846
|
-
| `MERIDIAN_DEFAULT_AGENT` | — | `opencode` | Default adapter for unrecognized agents: `opencode`, `forgecode`, `pi`, `crush`, `droid`, `cherry`, `claudecode`, `passthrough`. Requires restart. |
|
|
847
|
-
| `MERIDIAN_ROUTING` | — | `active` | Session-to-profile routing: `active` (all traffic to the active profile) or `sticky` ([sticky session routing](#sticky-session-routing)) |
|
|
848
|
-
| `MERIDIAN_PASSTHROUGH_EARLY_STOP` | — | `1` | Set to `0` to disable [digest-turn elimination](#how-tool-calling-works-in-passthrough) and restore the old end-of-turn behavior |
|
|
849
|
-
| `MERIDIAN_SUPPRESS_SCRATCHPAD` | — | `1` | Set to `0` to let the SDK advertise its proxy-host scratchpad directory in passthrough mode |
|
|
850
|
-
| `MERIDIAN_PRICING_CONFIG` | `CLAUDE_PROXY_PRICING_CONFIG` | `~/.config/meridian/model-pricing.json` | Path to the model pricing overrides file used by cost estimation |
|
|
851
|
-
| `MERIDIAN_PROFILES` | — | unset | JSON array of profile configs (overrides disk discovery). See [Multi-Profile Support](#multi-profile-support). |
|
|
852
|
-
| `MERIDIAN_DEFER_TOOL_THRESHOLD` | — | `15` | Number of tools before non-core tools are deferred via ToolSearch. Set to `0` to disable. |
|
|
853
|
-
| `MERIDIAN_TELEMETRY_PERSIST` | `CLAUDE_PROXY_TELEMETRY_PERSIST` | unset | Enable SQLite telemetry persistence. Data survives proxy restarts. |
|
|
854
|
-
| `MERIDIAN_TELEMETRY_DB` | `CLAUDE_PROXY_TELEMETRY_DB` | `~/.config/meridian/telemetry.db` | SQLite database path (when persistence is enabled) |
|
|
855
|
-
| `MERIDIAN_TELEMETRY_RETENTION_DAYS` | `CLAUDE_PROXY_TELEMETRY_RETENTION_DAYS` | `7` | Days to retain telemetry data before cleanup |
|
|
856
|
-
| `MERIDIAN_DEFAULT_PROFILE` | — | *(first profile)* | Default profile ID when no header is sent |
|
|
857
|
-
| `MERIDIAN_ADAPTER_INSTANCES` | — | unset | JSON [adapter instance](#adapter-instances) definitions, overriding `~/.config/meridian/adapter-instances.json` |
|
|
858
|
-
| `MERIDIAN_BETA_POLICY` | — | `allow-safe` | Client `anthropic-beta` header handling: `allow-safe`, `strip-all`, or `allow-all` |
|
|
859
|
-
| `MERIDIAN_DEFAULT_{FABLE,OPUS,SONNET,HAIKU}_MODEL` | — | canonical ids | Pin the model id the SDK resolves for each tier alias (e.g. `MERIDIAN_DEFAULT_OPUS_MODEL`) |
|
|
860
|
-
| `MERIDIAN_SESSION_DIR` | `CLAUDE_PROXY_SESSION_DIR` | `~/.cache/meridian` | Directory for the persisted session store |
|
|
861
|
-
| `MERIDIAN_DEBUG` | `CLAUDE_PROXY_DEBUG` | unset | Set to `1` for verbose request/session logging |
|
|
862
|
-
| `MERIDIAN_SILENT` | `CLAUDE_PROXY_SILENT` | unset | Set to `1` to suppress startup output (used by embedding plugins) |
|
|
863
|
-
| `MERIDIAN_PLUGIN_DIR` | — | `~/.config/meridian/plugins` | Plugin auto-discovery directory |
|
|
864
|
-
| `MERIDIAN_PLUGIN_CONFIG` | — | `~/.config/meridian/plugins.json` | Plugin manifest path |
|
|
865
|
-
|
|
866
|
-
†Sonnet 1M requires Extra Usage on all plans including Max ([docs](https://code.claude.com/docs/en/model-config#extended-context)). Opus 1M is included with Max/Team/Enterprise at no extra cost.
|
|
867
|
-
|
|
868
|
-
## Endpoints
|
|
869
|
-
|
|
870
|
-
| Endpoint | Description |
|
|
871
|
-
|----------|-------------|
|
|
872
|
-
| `GET /` | Landing page |
|
|
873
|
-
| `POST /v1/messages` | Anthropic Messages API |
|
|
874
|
-
| `POST /messages` | Alias for `/v1/messages` |
|
|
875
|
-
| `POST /v1/chat/completions` | OpenAI-compatible chat completions |
|
|
876
|
-
| `POST /v1/responses` | OpenAI Responses API (Codex CLI ≥ 0.96) |
|
|
877
|
-
| `GET /v1/models` | OpenAI-compatible model list |
|
|
878
|
-
| `GET/POST /v1/design/*` | Claude Design MCP proxy (see [Claude Design MCP](#claude-design-mcp)) |
|
|
879
|
-
| `GET/POST /design-login` | OAuth flow for the design scopes |
|
|
880
|
-
| `GET /health` | Auth status, mode, plugin status |
|
|
881
|
-
| `POST /auth/refresh` | Manually refresh the OAuth token |
|
|
882
|
-
| `GET /telemetry` | Performance dashboard |
|
|
883
|
-
| `GET /telemetry/requests` | Recent request metrics (JSON) |
|
|
884
|
-
| `GET /telemetry/summary` | Aggregate statistics (JSON) |
|
|
885
|
-
| `GET /telemetry/logs` | Diagnostic logs (JSON) |
|
|
886
|
-
| `GET /metrics` | Prometheus exposition format metrics |
|
|
887
|
-
| `GET /profiles` | Profile management page |
|
|
888
|
-
| `GET /profiles/list` | List profiles with auth status (JSON) |
|
|
889
|
-
| `POST /profiles/active` | Switch the active profile |
|
|
890
|
-
| `GET /v1/usage/quota` | Usage windows for the active profile (JSON) |
|
|
891
|
-
| `GET /v1/usage/quota/all` | Usage windows for every profile (JSON) |
|
|
892
|
-
| `GET /settings` | SDK feature toggles + model pricing UI |
|
|
893
|
-
| `GET /plugins` | Plugin management page (`/plugins/list`, `POST /plugins/reload` for JSON/actions) |
|
|
894
|
-
|
|
895
|
-
Health response example:
|
|
896
|
-
|
|
897
|
-
```json
|
|
898
|
-
{
|
|
899
|
-
"status": "healthy",
|
|
900
|
-
"version": "1.50.0",
|
|
901
|
-
"auth": { "loggedIn": true, "email": "you@example.com", "subscriptionType": "max" },
|
|
902
|
-
"mode": "internal",
|
|
903
|
-
"plugin": { "opencode": "configured" }
|
|
904
|
-
}
|
|
905
|
-
```
|
|
906
|
-
|
|
907
|
-
`plugin.opencode` is `"configured"` when `meridian setup` has been run, `"not-configured"` otherwise.
|
|
908
|
-
|
|
909
|
-
## Plugins
|
|
910
|
-
|
|
911
|
-
Extend Meridian's behavior with composable plugins — no core modifications needed.
|
|
912
|
-
|
|
913
|
-
**Quick start:** Drop a `.ts` or `.js` file in `~/.config/meridian/plugins/` and restart.
|
|
914
|
-
|
|
915
|
-
```ts
|
|
916
|
-
// ~/.config/meridian/plugins/my-plugin.ts
|
|
917
|
-
export default {
|
|
918
|
-
name: "my-plugin",
|
|
919
|
-
onRequest(ctx) {
|
|
920
|
-
// modify request context
|
|
921
|
-
return { ...ctx, systemContext: ctx.systemContext + "\nBe concise." }
|
|
922
|
-
},
|
|
923
|
-
}
|
|
924
|
-
```
|
|
925
|
-
|
|
926
|
-
- **Manage plugins** at `http://localhost:3456/plugins`
|
|
927
|
-
- **Reload without restart:** `POST /plugins/reload`
|
|
928
|
-
- **Full guide:** See [PLUGINS.md](PLUGINS.md)
|
|
929
|
-
|
|
930
|
-
### Official plugins
|
|
931
|
-
|
|
932
|
-
Content-scoped scrubbers maintained alongside Meridian. Core stays a clean
|
|
933
|
-
proxy — anything that rewrites client prompt content ships as one of these
|
|
934
|
-
opt-in plugins instead:
|
|
935
|
-
|
|
936
|
-
| Plugin | What it does |
|
|
937
|
-
|--------|--------------|
|
|
938
|
-
| [`@rynfar/meridian-plugin-hermes-scrub`](https://github.com/rynfar/meridian-plugin-hermes-scrub) | Strips Hermes Agent's `# Finishing the job` harness block from the system prompt. Fixes empty-stream responses when proxying Hermes, and avoids its coding-harness fingerprint. |
|
|
939
|
-
| [`@rynfar/meridian-plugin-pi-scrub`](https://github.com/rynfar/meridian-plugin-pi-scrub) | Strips Pi's coding-agent-harness prompt line that Anthropic meters as Extra Usage. |
|
|
940
|
-
| [`@rynfar/meridian-plugin-opencode-scrub`](https://github.com/rynfar/meridian-plugin-opencode-scrub) | Strips OpenCode harness boilerplate from the system prompt before it reaches Claude. |
|
|
941
|
-
|
|
942
|
-
**Nix users:** the flake packages all three prebuilt — `pkgs.meridianPlugins.<name>` via the `meridian` overlay (or `meridian.legacyPackages.${system}.meridianPlugins`), each exposing `.path` for a `plugins.json` entry or the home-manager `pluginConfig` option. Pins are refreshed by a scheduled workflow that rebuilds every plugin before bumping.
|
|
943
|
-
|
|
944
|
-
Everyone else: install into Meridian's config dir and register the built file in
|
|
945
|
-
`~/.config/meridian/plugins.json`:
|
|
946
|
-
|
|
947
|
-
```bash
|
|
948
|
-
cd ~/.config/meridian
|
|
949
|
-
npm install @rynfar/meridian-plugin-hermes-scrub
|
|
950
|
-
```
|
|
951
|
-
|
|
952
|
-
```json
|
|
953
|
-
{
|
|
954
|
-
"plugins": [
|
|
955
|
-
{ "path": "/Users/you/.config/meridian/node_modules/@rynfar/meridian-plugin-hermes-scrub/dist/index.js", "enabled": true }
|
|
956
|
-
]
|
|
957
|
-
}
|
|
958
|
-
```
|
|
959
|
-
|
|
960
|
-
Paths must be absolute — the loader does not expand `~`.
|
|
961
|
-
|
|
962
|
-
Both plugin locations are configurable for the standalone CLI: `MERIDIAN_PLUGIN_DIR` overrides the auto-discovery directory and `MERIDIAN_PLUGIN_CONFIG` the manifest path (useful for Nix, containers, or running several instances with different plugin sets).
|
|
963
|
-
|
|
964
|
-
## CLI Commands
|
|
965
|
-
|
|
966
|
-
| Command | Description |
|
|
967
|
-
|---------|-------------|
|
|
968
|
-
| `meridian` | Start the proxy server |
|
|
969
|
-
| `meridian setup` | Configure the OpenCode plugin in `~/.config/opencode/opencode.json` |
|
|
970
|
-
| `meridian profile add <name> [--headless]` | Add a profile and authenticate via Claude OAuth; `--headless` prints a URL, prompts for the returned code, and stores the exchanged credentials |
|
|
971
|
-
| `meridian profile add <name> --oauth-token [TOKEN]` | Add a headless profile from a `claude setup-token` value (prompts when `TOKEN` is omitted) |
|
|
972
|
-
| `meridian profile list` (alias `profile ls`) | List all profiles and their auth status |
|
|
973
|
-
| `meridian profile switch <name>` | Switch the active profile (requires running proxy) |
|
|
974
|
-
| `meridian profile login <name> [--headless]` | Re-authenticate an expired profile (browser-login profiles only); `--headless` uses the URL/code flow |
|
|
975
|
-
| `meridian profile remove <name>` | Remove a profile and its credentials |
|
|
976
|
-
| `meridian refresh-token` | Manually refresh the Claude OAuth token (exits 0/1) |
|
|
977
|
-
|
|
978
|
-
## Programmatic API
|
|
979
|
-
|
|
980
|
-
```typescript
|
|
981
|
-
import { startProxyServer } from "@rynfar/meridian"
|
|
982
|
-
|
|
983
|
-
const instance = await startProxyServer({
|
|
984
|
-
port: 3456,
|
|
985
|
-
host: "127.0.0.1",
|
|
986
|
-
silent: true,
|
|
987
|
-
})
|
|
988
|
-
|
|
989
|
-
// instance.server — underlying http.Server
|
|
990
|
-
await instance.close()
|
|
991
|
-
```
|
|
992
|
-
|
|
993
|
-
## Docker
|
|
994
|
-
|
|
995
|
-
Claude Code authentication requires a browser, which isn't available inside containers. Authenticate on your local machine first, then mount the credentials into Docker.
|
|
996
|
-
|
|
997
|
-
### Single account
|
|
998
|
-
|
|
999
|
-
```bash
|
|
1000
|
-
# 1. Authenticate locally (one time)
|
|
1001
|
-
claude login
|
|
1002
|
-
|
|
1003
|
-
# 2. Run with mounted credentials
|
|
1004
|
-
docker run -v ~/.claude:/home/claude/.claude -p 3456:3456 meridian
|
|
1005
|
-
```
|
|
1006
|
-
|
|
1007
|
-
Meridian refreshes OAuth tokens automatically — once the credentials are mounted, no further browser access is needed.
|
|
1008
|
-
|
|
1009
|
-
> **macOS hosts:** mounting `~/.claude` does **not** carry credentials into the container — on macOS the CLI stores OAuth tokens in the Keychain, not in files, so the container sees an empty credential store and requests fail with an authentication error. Use an [OAuth-token profile](#oauth-token-profiles-in-docker-no-volume-mount) instead (recommended), or run `claude login` once inside the container (`docker exec -it <name> claude login`).
|
|
1010
|
-
|
|
1011
|
-
### Multiple profiles in Docker
|
|
1012
|
-
|
|
1013
|
-
Authenticate each profile locally, then pass them to Docker via the `MERIDIAN_PROFILES` environment variable:
|
|
1014
|
-
|
|
1015
|
-
```bash
|
|
1016
|
-
# 1. Authenticate each account locally
|
|
1017
|
-
meridian profile add personal
|
|
1018
|
-
meridian profile add work # sign out of claude.ai first, sign into work account
|
|
1019
|
-
|
|
1020
|
-
# 2. Run Docker with profile configs pointing to mounted credential directories
|
|
1021
|
-
docker run \
|
|
1022
|
-
-v ~/.config/meridian/profiles/personal:/profiles/personal \
|
|
1023
|
-
-v ~/.config/meridian/profiles/work:/profiles/work \
|
|
1024
|
-
-e 'MERIDIAN_PROFILES=[{"id":"personal","claudeConfigDir":"/profiles/personal"},{"id":"work","claudeConfigDir":"/profiles/work"}]' \
|
|
1025
|
-
-e MERIDIAN_DEFAULT_PROFILE=personal \
|
|
1026
|
-
-p 3456:3456 meridian
|
|
1027
|
-
```
|
|
1028
|
-
|
|
1029
|
-
Switch profiles at runtime via the `x-meridian-profile` header or `meridian profile switch` (see [Multi-Profile Support](#multi-profile-support)).
|
|
1030
|
-
|
|
1031
|
-
### OAuth-token profiles in Docker (no volume mount)
|
|
1032
|
-
|
|
1033
|
-
If you'd rather not mount a credential directory, generate a long-lived OAuth token on the host with `claude setup-token` and pass it as a profile. There's nothing to mount — the token alone is the credential:
|
|
1034
|
-
|
|
1035
|
-
```bash
|
|
1036
|
-
docker run \
|
|
1037
|
-
-e 'MERIDIAN_PROFILES=[{"id":"ci","oauthToken":"sk-ant-oat01-..."}]' \
|
|
1038
|
-
-e MERIDIAN_DEFAULT_PROFILE=ci \
|
|
1039
|
-
-p 3456:3456 meridian
|
|
1040
|
-
```
|
|
1041
|
-
|
|
1042
|
-
This is the recommended path for CI runners, ephemeral containers, and cross-host deployments where browser-based login isn't reachable. Treat the token like any other secret — inject it via your platform's secret store rather than committing it to your image or compose file.
|
|
1043
|
-
|
|
1044
|
-
## Testing
|
|
1045
|
-
|
|
1046
|
-
```bash
|
|
1047
|
-
npm test # unit + integration tests
|
|
1048
|
-
npm run build # build with bun + tsc
|
|
1049
|
-
```
|
|
1050
|
-
|
|
1051
|
-
| Tier | What | Speed |
|
|
1052
|
-
|------|------|-------|
|
|
1053
|
-
| Unit | Pure functions, no mocks | Fast |
|
|
1054
|
-
| Integration | HTTP layer with mocked SDK | Fast |
|
|
1055
|
-
| E2E | Real proxy + real Claude Max ([`E2E.md`](E2E.md)) | Manual |
|
|
1056
|
-
|
|
1057
114
|
## FAQ
|
|
1058
115
|
|
|
1059
116
|
**Is this allowed by Anthropic's terms?**
|
|
@@ -1078,7 +135,7 @@ curl -X POST http://127.0.0.1:3456/auth/refresh
|
|
|
1078
135
|
**I'm getting `400 You're out of extra usage` on tool-bearing requests. What do I do?**
|
|
1079
136
|
This error class ([#516](https://github.com/rynfar/meridian/issues/516), historical) came from Anthropic's server-side classifier gating certain requests behind Extra Usage. It had two distinct triggers, both now addressed:
|
|
1080
137
|
|
|
1081
|
-
- **Harness fingerprints** — identity lines in a client's system prompt (e.g. pi's "coding agent harness" line) were metered as Extra Usage. The [official scrub plugins](#official-plugins) strip these and remain recommended for the affected harnesses.
|
|
138
|
+
- **Harness fingerprints** — identity lines in a client's system prompt (e.g. pi's "coding agent harness" line) were metered as Extra Usage. The [official scrub plugins](docs/plugins.md#official-plugins) strip these and remain recommended for the affected harnesses.
|
|
1082
139
|
- **Tool-definition presence** — reported in mid-2026 as triggering independently of prompt content; as of July 2026 this no longer reproduces on Max accounts (verified with Extra Usage disabled, tools present, and an unscrubbed fingerprint prompt). It appears to have been resolved upstream in Anthropic's billing policy.
|
|
1083
140
|
|
|
1084
141
|
If you still hit the error on a current release, first check `GET /v1/usage/quota` to rule out genuinely exhausted quota, then try disabling the connecting client's system prompt for the affected adapter while keeping the Claude Code prompt enabled (in the `/settings` UI under **SDK Feature Toggles**, or `PATCH /settings/api/features/<adapter>` with `{"clientSystemPrompt":false,"codeSystemPrompt":true}`) — and please report it on [#516](https://github.com/rynfar/meridian/issues/516) with your plan type, since remaining occurrences are likely account-cohort specific (Team plans are treated differently by the API).
|
|
@@ -9988,7 +9988,7 @@ function introSection(h){
|
|
|
9988
9988
|
meta.push('port '+location.port);
|
|
9989
9989
|
return '<div class="intro">'
|
|
9990
9990
|
+'<h2>Harness Claude, your way.</h2>'
|
|
9991
|
-
+'<p>Meridian bridges any Anthropic-API agent to your Claude subscription — point the agent’s <code>ANTHROPIC_BASE_URL</code> at <code>http://'+esc(location.host)+'</code> and every request routes through the active account below. Setup guides for each agent live in the <a href="https://github.com/rynfar/meridian
|
|
9991
|
+
+'<p>Meridian bridges any Anthropic-API agent to your Claude subscription — point the agent’s <code>ANTHROPIC_BASE_URL</code> at <code>http://'+esc(location.host)+'</code> and every request routes through the active account below. Setup guides for each agent live in the <a href="https://github.com/rynfar/meridian/blob/main/docs/agents.md">Agent Setup guide</a>.</p>'
|
|
9992
9992
|
+'<div class="intro-meta">'+meta.join(' · ')+'</div>'
|
|
9993
9993
|
+'</div>';
|
|
9994
9994
|
}
|
|
@@ -12330,8 +12330,8 @@ function extractPiCwd(body) {
|
|
|
12330
12330
|
}
|
|
12331
12331
|
var piAdapter = {
|
|
12332
12332
|
name: "pi",
|
|
12333
|
-
getSessionId(
|
|
12334
|
-
return;
|
|
12333
|
+
getSessionId(c) {
|
|
12334
|
+
return c.req.header("x-session-affinity");
|
|
12335
12335
|
},
|
|
12336
12336
|
extractWorkingDirectory(body) {
|
|
12337
12337
|
return extractPiCwd(body);
|
|
@@ -12596,6 +12596,9 @@ var codexAdapter = {
|
|
|
12596
12596
|
name: "codex",
|
|
12597
12597
|
usesPassthrough() {
|
|
12598
12598
|
return true;
|
|
12599
|
+
},
|
|
12600
|
+
getSessionId(c) {
|
|
12601
|
+
return c.req.header("x-codex-session");
|
|
12599
12602
|
}
|
|
12600
12603
|
};
|
|
12601
12604
|
|
|
@@ -20013,7 +20016,7 @@ function createProxyServer(config = {}) {
|
|
|
20013
20016
|
const lastMessage = Array.isArray(body.messages) ? body.messages[body.messages.length - 1] : undefined;
|
|
20014
20017
|
const lastIsToolResult = Array.isArray(lastMessage?.content) && lastMessage.content.some((b) => b?.type === "tool_result");
|
|
20015
20018
|
const isClientDrivenLoop = adapterBase !== "claude-code" && !agentSessionId && lastIsToolResult;
|
|
20016
|
-
const isIndependentSession = requestSource?.startsWith("fork-") || requestSource?.startsWith("subagent-") || isClientDrivenLoop || false;
|
|
20019
|
+
const isIndependentSession = !agentSessionId && (requestSource?.startsWith("fork-") || requestSource?.startsWith("subagent-")) || isClientDrivenLoop || false;
|
|
20017
20020
|
if (!isIndependentSession && profileSessionId) {
|
|
20018
20021
|
const pendingStore = pendingSessionStores.get(profileSessionId);
|
|
20019
20022
|
if (pendingStore) {
|
|
@@ -22231,6 +22234,10 @@ data: ${JSON.stringify({
|
|
|
22231
22234
|
"Content-Type": "application/json",
|
|
22232
22235
|
"x-meridian-agent": "codex"
|
|
22233
22236
|
};
|
|
22237
|
+
const promptCacheKey = rawBody.prompt_cache_key;
|
|
22238
|
+
if (typeof promptCacheKey === "string" && promptCacheKey.length > 0) {
|
|
22239
|
+
internalHeaders["x-codex-session"] = promptCacheKey;
|
|
22240
|
+
}
|
|
22234
22241
|
const xApiKey = c.req.header("x-api-key");
|
|
22235
22242
|
if (xApiKey)
|
|
22236
22243
|
internalHeaders["x-api-key"] = xApiKey;
|
package/dist/cli.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../../src/proxy/adapters/codex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAG9C,eAAO,MAAM,YAAY,EAAE,
|
|
1
|
+
{"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../../src/proxy/adapters/codex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAG9C,eAAO,MAAM,YAAY,EAAE,YAgB1B,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pi.d.ts","sourceRoot":"","sources":["../../../src/proxy/adapters/pi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAyC9C,eAAO,MAAM,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"pi.d.ts","sourceRoot":"","sources":["../../../src/proxy/adapters/pi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAyC9C,eAAO,MAAM,SAAS,EAAE,YA+HvB,CAAA;AAED,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAGvD,YAAY,EACV,SAAS,EACT,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,WAAW,GACZ,MAAM,aAAa,CAAA;AAKpB,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAkDnG,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EAEpB,KAAK,aAAa,EAGnB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAA+B,iBAAiB,EAAE,mBAAmB,EAAsC,MAAM,iBAAiB,CAAA;AAGzI,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,CAAA;AACjD,YAAY,EAAE,aAAa,EAAE,CAAA;AAgR7B,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAGvD,YAAY,EACV,SAAS,EACT,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,WAAW,GACZ,MAAM,aAAa,CAAA;AAKpB,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAkDnG,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EAEpB,KAAK,aAAa,EAGnB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAA+B,iBAAiB,EAAE,mBAAmB,EAAsC,MAAM,iBAAiB,CAAA;AAGzI,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,CAAA;AACjD,YAAY,EAAE,aAAa,EAAE,CAAA;AAgR7B,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CAysHhF;AAWD,wBAAgB,gCAAgC,IAAI,IAAI,CAavD;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAmGhG"}
|
package/dist/server.js
CHANGED
package/package.json
CHANGED