@rynfar/meridian 1.29.0 → 1.29.2

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 CHANGED
@@ -7,14 +7,34 @@
7
7
  <a href="https://www.npmjs.com/package/@rynfar/meridian"><img src="https://img.shields.io/npm/v/@rynfar/meridian?style=flat-square&color=8b5cf6&label=npm" alt="npm"></a>
8
8
  <a href="#"><img src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-a78bfa?style=flat-square" alt="Platform"></a>
9
9
  <a href="#"><img src="https://img.shields.io/badge/license-MIT-c4b5fd?style=flat-square" alt="License"></a>
10
+ <a href="https://discord.gg/7vNVFYBz"><img src="https://img.shields.io/badge/discord-join-5865F2?style=flat-square&logo=discord&logoColor=white" alt="Discord"></a>
10
11
  </p>
11
12
 
12
13
  ---
13
14
 
14
- Meridian turns your Claude Max subscription into a local Anthropic API. Any tool that speaks the Anthropic or OpenAI protocol — OpenCode, OpenClaw, Crush, Cline, Aider, Pi, Droid, Open WebUI — connects to Meridian and gets Claude, powered by your existing subscription through the official Claude Code SDK.
15
-
16
- > [!IMPORTANT]
17
- > **Extra Usage billing fix (v0.x.x):** Previous versions defaulted Sonnet to `sonnet[1m]` (1M context), which is [always billed as Extra Usage](https://code.claude.com/docs/en/model-config#extended-context) on Max plans — even when regular usage isn't exhausted. Sonnet now defaults to 200k. If you're on an older version, update or set `MERIDIAN_SONNET_MODEL=sonnet` as a workaround. See [#255](https://github.com/rynfar/meridian/issues/255) for details.
15
+ Meridian bridges the Claude Code SDK to the standard Anthropic API. No OAuth interception. No binary patches. No hacks. Just pure, documented SDK calls. Any tool that speaks the Anthropic or OpenAI protocol — OpenCode, Crush, Cline, Aider, Pi, Droid, Open WebUI — connects to Meridian and gets Claude, with session management, streaming, and prompt caching handled natively by the SDK.
16
+
17
+ > [!NOTE]
18
+ > ### How Meridian works with Anthropic
19
+ >
20
+ > Meridian is built entirely on the [Claude Code SDK](https://docs.anthropic.com/en/docs/claude-code/sdk). Every request flows through `query()` — the same documented function Anthropic provides for programmatic access. No OAuth tokens are extracted, no binaries are patched, nothing is reverse-engineered.
21
+ >
22
+ > Because we use the SDK, Anthropic remains in full control of prompt caching, context window management, compaction, rate limiting, and authentication. Meridian doesn't bypass these mechanisms — it depends on them. Max subscription tokens flow through the correct channel, governed by the same guardrails Anthropic built into Claude Code.
23
+ >
24
+ > What Meridian adds is a **presentation and interoperability layer**. We translate Claude Code's output into the standard Anthropic API format so developers can connect the editors, terminals, and workflows they prefer. The SDK does the work; Meridian formats the result.
25
+ >
26
+ > If you're looking for a tool that circumvents usage limits or bypasses Anthropic's controls, this project is not for you. We play nice with the SDK because we believe that's how developers can continue to choose their own frontends while respecting Anthropic's platform.
27
+
28
+ > [!WARNING]
29
+ > ### Why Meridian does not support OpenClaw
30
+ >
31
+ > There is technically a way to make Meridian work with OpenClaw, but we're not interested in pursuing it.
32
+ >
33
+ > The reason Claude Max offers generous usage limits is because Anthropic can justify it through Claude Code — their harness, their optimizations, their control. OpenClaw blows through that with autonomous workflows that Anthropic has little ability to manage or optimize. Using Opus to check an email when a local model would handle it fine isn't efficient use — it's waste that degrades the plan for everyone.
34
+ >
35
+ > I built Meridian because I believe developers should have the right to use the frontend of their choice. But that right comes with a responsibility: don't wreck the subscription for the rest of us. Sloppy autonomous agents that burn through Claude Max tokens are directly counter-productive to developers like me who depend on the plan being sustainable.
36
+ >
37
+ > Meridian's philosophy is simple — play nice with the SDK, let Anthropic optimize how they see fit, and use the frontend you want within the constraints of Claude Code. OpenClaw is not just a frontend; it's an autonomous system that abuses the Max plan. We won't be supporting it.
18
38
 
19
39
  ## Quick Start
20
40
 
@@ -38,13 +58,11 @@ Meridian runs on `http://127.0.0.1:3456`. Point any Anthropic-compatible tool at
38
58
  ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://127.0.0.1:3456 opencode
39
59
  ```
40
60
 
41
- The API key value doesn't matter — Meridian authenticates through your Claude Max session, not API keys.
61
+ 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.
42
62
 
43
63
  ## Why Meridian?
44
64
 
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.
46
-
47
- Meridian bridges that gap. It runs locally, accepts standard Anthropic API requests, and routes them through the SDK using your Max subscription.
65
+ The Claude Code SDK provides programmatic access to Claude. But your favorite coding tools expect an Anthropic API endpoint. Meridian bridges that gap — it runs locally, accepts standard API requests, and routes them through the SDK. Claude Code does the heavy lifting; Meridian translates the output.
48
66
 
49
67
  <p align="center">
50
68
  <img src="assets/how-it-works.svg" alt="How Meridian works" width="920"/>
@@ -275,29 +293,6 @@ MERIDIAN_DEFAULT_AGENT=pi meridian
275
293
 
276
294
  Pi mimics Claude Code's User-Agent, so automatic detection isn't possible. The `MERIDIAN_DEFAULT_AGENT` env var tells Meridian to use the pi adapter for all unrecognized requests. If you run other agents alongside pi, use the `x-meridian-agent: pi` header instead (requires pi-ai support for custom headers).
277
295
 
278
- ### OpenClaw
279
-
280
- OpenClaw uses `@mariozechner/pi-ai` under the hood, so the pi adapter handles it with no additional code. Add a provider override in `~/.openclaw/openclaw.json`:
281
-
282
- ```json
283
- {
284
- "models": {
285
- "providers": {
286
- "anthropic": {
287
- "baseUrl": "http://127.0.0.1:3456",
288
- "apiKey": "dummy",
289
- "models": [
290
- { "id": "claude-sonnet-4-6", "name": "Claude Sonnet 4.6 (Meridian)" },
291
- { "id": "claude-opus-4-6", "name": "Claude Opus 4.6 (Meridian)" }
292
- ]
293
- }
294
- }
295
- }
296
- }
297
- ```
298
-
299
- Then start Meridian with the pi adapter: `MERIDIAN_DEFAULT_AGENT=pi meridian`
300
-
301
296
  ### Any Anthropic-compatible tool
302
297
 
303
298
  ```bash
@@ -316,7 +311,6 @@ export ANTHROPIC_BASE_URL=http://127.0.0.1:3456
316
311
  | [Aider](https://github.com/paul-gauthier/aider) | ✅ Verified | Env vars — file editing, streaming; `--no-stream` broken (litellm bug) |
317
312
  | [Open WebUI](https://github.com/open-webui/open-webui) | ✅ Verified | OpenAI-compatible endpoints — set base URL to `http://127.0.0.1:3456` |
318
313
  | [Pi](https://github.com/mariozechner/pi-coding-agent) | ✅ Verified | models.json config (see above) — requires `MERIDIAN_DEFAULT_AGENT=pi` |
319
- | [OpenClaw](https://github.com/openclaw/openclaw) | ✅ Verified | Provider config (see above) — uses pi adapter via `MERIDIAN_DEFAULT_AGENT=pi` |
320
314
  | [Continue](https://github.com/continuedev/continue) | 🔲 Untested | OpenAI-compatible endpoints should work — set `apiBase` to `http://127.0.0.1:3456` |
321
315
 
322
316
  Tested an agent or built a plugin? [Open an issue](https://github.com/rynfar/meridian/issues) and we'll add it.
@@ -488,10 +482,10 @@ npm run build # build with bun + tsc
488
482
  ## FAQ
489
483
 
490
484
  **Is this allowed by Anthropic's terms?**
491
- 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.
485
+ Meridian uses the official Claude Code SDK — the same SDK Anthropic publishes and documents for programmatic access. It does not intercept credentials, modify binaries, or bypass any authentication. All requests flow through the SDK's own authentication and rate-limiting mechanisms.
492
486
 
493
487
  **How is this different from using an API key?**
494
- API keys are billed per token. Claude Max is a flat monthly fee. Meridian lets you use that subscription from any compatible tool.
488
+ API keys provide direct API access billed per token. Claude Max includes programmatic access through the Claude Code SDK. Meridian translates SDK responses into the standard Anthropic API format, allowing compatible tools to connect through Claude Code.
495
489
 
496
490
  **What happens if my OAuth token expires?**
497
491
  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`.
@@ -514,7 +508,7 @@ You haven't run `meridian setup`. Without the plugin, OpenCode requests won't ha
514
508
 
515
509
  ## Contributing
516
510
 
517
- Issues and PRs welcome. See [`ARCHITECTURE.md`](ARCHITECTURE.md) for module structure and dependency rules, [`CLAUDE.md`](CLAUDE.md) for coding guidelines, and [`E2E.md`](E2E.md) for end-to-end test procedures.
511
+ Issues and PRs welcome. Join the [Discord](https://discord.gg/7vNVFYBz) to discuss ideas before opening issues. See [`ARCHITECTURE.md`](ARCHITECTURE.md) for module structure and dependency rules, [`CLAUDE.md`](CLAUDE.md) for coding guidelines, and [`E2E.md`](E2E.md) for end-to-end test procedures.
518
512
 
519
513
  ## License
520
514
 
@@ -13833,6 +13833,50 @@ function buildQueryOptions(ctx) {
13833
13833
  };
13834
13834
  }
13835
13835
 
13836
+ // src/proxy/betas.ts
13837
+ var BILLABLE_BETA_PREFIXES_ON_MAX = [
13838
+ "extended-cache-ttl-"
13839
+ ];
13840
+ var DEFAULT_BETA_POLICY = "allow-safe";
13841
+ function getBetaPolicyFromEnv() {
13842
+ const raw2 = process.env.MERIDIAN_BETA_POLICY;
13843
+ if (raw2 === "allow-safe" || raw2 === "strip-all" || raw2 === "allow-all") {
13844
+ return raw2;
13845
+ }
13846
+ return DEFAULT_BETA_POLICY;
13847
+ }
13848
+ function filterBetasForProfile(rawBetaHeader, profileType, policy = DEFAULT_BETA_POLICY) {
13849
+ if (!rawBetaHeader) {
13850
+ return { forwarded: undefined, stripped: [] };
13851
+ }
13852
+ const parsed = rawBetaHeader.split(",").map((b) => b.trim()).filter(Boolean);
13853
+ if (parsed.length === 0) {
13854
+ return { forwarded: undefined, stripped: [] };
13855
+ }
13856
+ if (profileType === "api") {
13857
+ return { forwarded: parsed, stripped: [] };
13858
+ }
13859
+ if (policy === "allow-all") {
13860
+ return { forwarded: parsed, stripped: [] };
13861
+ }
13862
+ if (policy === "strip-all") {
13863
+ return { forwarded: undefined, stripped: parsed };
13864
+ }
13865
+ const forwarded = [];
13866
+ const stripped = [];
13867
+ for (const beta of parsed) {
13868
+ if (BILLABLE_BETA_PREFIXES_ON_MAX.some((prefix) => beta.startsWith(prefix))) {
13869
+ stripped.push(beta);
13870
+ } else {
13871
+ forwarded.push(beta);
13872
+ }
13873
+ }
13874
+ return {
13875
+ forwarded: forwarded.length > 0 ? forwarded : undefined,
13876
+ stripped
13877
+ };
13878
+ }
13879
+
13836
13880
  // src/proxy/session/lineage.ts
13837
13881
  import { createHash as createHash2 } from "crypto";
13838
13882
  var MIN_SUFFIX_FOR_COMPACTION = 2;
@@ -14613,7 +14657,11 @@ function createProxyServer(config = {}) {
14613
14657
  const effortHeader = c.req.header("x-opencode-effort");
14614
14658
  const thinkingHeader = c.req.header("x-opencode-thinking");
14615
14659
  const taskBudgetHeader = c.req.header("x-opencode-task-budget");
14616
- const betaHeader = profile.type === "api" ? c.req.header("anthropic-beta") : undefined;
14660
+ const rawBetaHeader = c.req.header("anthropic-beta");
14661
+ const betaFilter = filterBetasForProfile(rawBetaHeader, profile.type, getBetaPolicyFromEnv());
14662
+ if (betaFilter.stripped.length > 0) {
14663
+ console.error(`[PROXY] ${requestMeta.requestId} stripped anthropic-beta(s) for Max profile: ${betaFilter.stripped.join(", ")}`);
14664
+ }
14617
14665
  const effort = effortHeader || body.effort || undefined;
14618
14666
  let thinking = body.thinking || undefined;
14619
14667
  if (thinkingHeader !== undefined) {
@@ -14625,10 +14673,7 @@ function createProxyServer(config = {}) {
14625
14673
  }
14626
14674
  const parsedBudget = taskBudgetHeader ? Number.parseInt(taskBudgetHeader, 10) : NaN;
14627
14675
  const taskBudget = Number.isFinite(parsedBudget) ? { total: parsedBudget } : body.task_budget ? { total: body.task_budget.total ?? body.task_budget } : undefined;
14628
- const betas = betaHeader ? betaHeader.split(",").map((b) => b.trim()).filter(Boolean) : undefined;
14629
- if (!betaHeader && c.req.header("anthropic-beta")) {
14630
- console.error(`[PROXY] ${requestMeta.requestId} stripped anthropic-beta header (Max subscription — betas trigger extra usage billing)`);
14631
- }
14676
+ const betas = betaFilter.forwarded;
14632
14677
  const agentSessionId = adapter.getSessionId(c);
14633
14678
  const profileSessionId = profile.id !== "default" && agentSessionId ? `${profile.id}:${agentSessionId}` : agentSessionId;
14634
14679
  const profileScopedCwd = profile.id !== "default" ? `${workingDirectory}::profile=${profile.id}` : workingDirectory;
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startProxyServer
4
- } from "./cli-msyx6dnk.js";
4
+ } from "./cli-trtwsfge.js";
5
5
  import"./cli-g9ypdz51.js";
6
6
  import"./cli-rtab0qa6.js";
7
7
  import"./cli-m9pfb7h9.js";
@@ -0,0 +1,70 @@
1
+ /**
2
+ * anthropic-beta header filtering for Max vs API profiles.
3
+ *
4
+ * Some betas (e.g. `extended-cache-ttl-*`) trigger Extra-Usage billing on
5
+ * Claude Max subscriptions. The default `allow-safe` policy strips only those
6
+ * for claude-max profiles while forwarding everything else so that prompt
7
+ * caching, 1M context, fine-grained tool streaming, and interleaved thinking
8
+ * continue to work as the SDK expects.
9
+ *
10
+ * Unconditional stripping (the previous behaviour) caused cache misses on
11
+ * every turn, which tripled TTFB and inflated token consumption roughly 3x on
12
+ * long conversations. See issue #278 for the original context.
13
+ *
14
+ * An operator can override the policy at runtime via the `MERIDIAN_BETA_POLICY`
15
+ * env var to force `strip-all` (safest — old behaviour) or `allow-all`
16
+ * (most permissive — matches api-profile behaviour) without a rebuild.
17
+ *
18
+ * This module is pure — no I/O, no imports from server.ts or session/.
19
+ */
20
+ import type { ProfileType } from "./profiles";
21
+ /**
22
+ * Beta prefixes that are known to trigger Extra-Usage billing on Max accounts.
23
+ *
24
+ * A beta is considered billable if its name starts with any of these strings.
25
+ * Keep this list conservative — prefer allowing unknown betas through over
26
+ * silently stripping something the SDK needs for normal operation.
27
+ */
28
+ export declare const BILLABLE_BETA_PREFIXES_ON_MAX: readonly string[];
29
+ /**
30
+ * Runtime policy for `anthropic-beta` header handling on claude-max profiles.
31
+ *
32
+ * - `allow-safe` (default): forward all betas except those matching
33
+ * {@link BILLABLE_BETA_PREFIXES_ON_MAX}. Restores prompt caching + 1M
34
+ * context while keeping the original billing-safety intent.
35
+ * - `strip-all`: the pre-fix (1.28.0 – 1.29.x) behaviour. Drops every beta
36
+ * for claude-max profiles. Use this as a kill switch if the allow-safe
37
+ * policy ever causes quota surprises.
38
+ * - `allow-all`: forward every beta unconditionally, same as api profiles.
39
+ * Use only if you've verified your Max tier treats all betas as free.
40
+ */
41
+ export type BetaPolicy = "allow-safe" | "strip-all" | "allow-all";
42
+ export declare const DEFAULT_BETA_POLICY: BetaPolicy;
43
+ export interface BetaFilterResult {
44
+ /** Betas to forward upstream. `undefined` means no header should be sent. */
45
+ forwarded: string[] | undefined;
46
+ /** Betas that were removed. Empty for api-type profiles. */
47
+ stripped: string[];
48
+ }
49
+ /**
50
+ * Read the beta policy from the `MERIDIAN_BETA_POLICY` env var.
51
+ *
52
+ * Falls back to {@link DEFAULT_BETA_POLICY} for missing or invalid values.
53
+ * Invalid values are silently ignored rather than crashing the proxy.
54
+ */
55
+ export declare function getBetaPolicyFromEnv(): BetaPolicy;
56
+ /**
57
+ * Filter an `anthropic-beta` header value for the given profile type.
58
+ *
59
+ * - For `api` profiles, all betas pass through unchanged regardless of policy.
60
+ * - For `claude-max` profiles, behaviour depends on `policy`:
61
+ * - `allow-safe` (default): strip only billable betas
62
+ * (see {@link BILLABLE_BETA_PREFIXES_ON_MAX}).
63
+ * - `strip-all`: strip every beta.
64
+ * - `allow-all`: forward every beta unchanged.
65
+ * - Whitespace and empty entries are trimmed.
66
+ * - Returns `forwarded: undefined` when the result would be an empty list so
67
+ * callers can omit the header entirely.
68
+ */
69
+ export declare function filterBetasForProfile(rawBetaHeader: string | undefined, profileType: ProfileType, policy?: BetaPolicy): BetaFilterResult;
70
+ //# sourceMappingURL=betas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"betas.d.ts","sourceRoot":"","sources":["../../src/proxy/betas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,EAAE,SAAS,MAAM,EAE1D,CAAA;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,WAAW,GAAG,WAAW,CAAA;AAEjE,eAAO,MAAM,mBAAmB,EAAE,UAAyB,CAAA;AAE3D,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,SAAS,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;IAC/B,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,UAAU,CAMjD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACnC,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,WAAW,EAAE,WAAW,EACxB,MAAM,GAAE,UAAgC,GACvC,gBAAgB,CA0ClB"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAqBvD,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EACpB,KAAK,aAAa,EAEnB,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;AAoG7B,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CA0pDhF;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAiEhG"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAsBvD,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EACpB,KAAK,aAAa,EAEnB,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;AAoG7B,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CA8pDhF;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAiEhG"}
package/dist/server.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  getMaxSessionsLimit,
7
7
  hashMessage,
8
8
  startProxyServer
9
- } from "./cli-msyx6dnk.js";
9
+ } from "./cli-trtwsfge.js";
10
10
  import"./cli-g9ypdz51.js";
11
11
  import"./cli-rtab0qa6.js";
12
12
  import"./cli-m9pfb7h9.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynfar/meridian",
3
- "version": "1.29.0",
3
+ "version": "1.29.2",
4
4
  "description": "Local Anthropic API powered by your Claude Max subscription. One subscription, every agent.",
5
5
  "type": "module",
6
6
  "main": "./dist/server.js",