@rynfar/meridian 1.29.1 → 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,31 +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 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, OpenClaw, 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.
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.
15
16
 
16
- > [!IMPORTANT]
17
- > ### Meridian is unaffected by the April 5, 2025 third-party blocks
17
+ > [!NOTE]
18
+ > ### How Meridian works with Anthropic
18
19
  >
19
- > On April 5, 2025, Anthropic began blocking third-party tools that bypass Claude Code by intercepting OAuth tokens and replaying them against internal API endpoints. Tools that extract `~/.claude/` credentials, proxy raw OAuth bearer tokens, or patch Claude Code binaries to redirect traffic may no longer function.
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.
20
21
  >
21
- > **Meridian does not do any of this.** Its architecture is fundamentally different:
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.
22
23
  >
23
- > - **SDK-native.** Every request calls [`query()`](https://docs.anthropic.com/en/docs/claude-code/sdk) from `@anthropic-ai/claude-agent-sdk` the same function Anthropic documents for programmatic Claude Code access. No OAuth tokens are extracted, intercepted, or replayed.
24
- > - **Real Claude Code sessions.** The SDK spawns the actual Claude Code process, manages its own authentication, and handles all communication with Anthropic's servers. Meridian's traffic doesn't *look like* Claude Code — it *is* Claude Code.
25
- > - **Documented API surface only.** Session resume, MCP tool servers, agent definitions, thinking configuration, permission modes, tool blocking — every feature Meridian uses is a published, documented SDK option. Nothing is reverse-engineered or patched.
26
- > - **Native benefits and controls preserved.** Prompt caching, conversation persistence, context window management, and compaction all function exactly as they do in Claude Code — because the SDK manages them directly. This means Anthropic's engineering investments in efficiency and their rate-limiting controls work as designed. Max subscription tokens flow through the correct channel, governed by the same guardrails Anthropic built into Claude Code. Meridian doesn't bypass these mechanisms; it depends on them.
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.
27
25
  >
28
- > A small number of adjustments were made in response to the April 5th changes notably stripping `anthropic-beta` headers that could trigger unintended Extra Usage billing on Max subscriptions ([#281](https://github.com/rynfar/meridian/issues/281)). We are also evaluating system prompt handling to ensure nothing conflicts with Claude Code's expectations. These are compatibility adjustments, not workarounds. Our philosophy is to let Claude Code be the foundation and never fight the SDK — we work with it and add our own layer on top.
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.
29
32
  >
30
- > **Our position is straightforward.** Anthropic asks developers to use Claude Code as the harness for Max subscription access we do. We call their SDK, respect its authentication flow, use its documented features, and operate within its designed boundaries. We are not circumventing Claude Code; we are building on top of it.
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.
31
34
  >
32
- > 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. Developers should have the right to choose their own interface and integrate with their own tooling that's not circumvention, it's the reason SDKs exist.
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.
33
36
  >
34
- > For Meridian to stop working, Anthropic would need to restrict the Claude Code SDK itself or remove documented features that legitimate SDK consumers depend on. We don't believe that's the intent. We're building within Anthropic's ecosystem and constraints because we genuinely value their tools and models. We simply want the freedom to choose how we experience them — and we hope Anthropic sees that as the kind of ecosystem engagement their SDK was designed to enable.
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.
35
38
 
36
39
  ## Quick Start
37
40
 
@@ -290,29 +293,6 @@ MERIDIAN_DEFAULT_AGENT=pi meridian
290
293
 
291
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).
292
295
 
293
- ### OpenClaw
294
-
295
- 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`:
296
-
297
- ```json
298
- {
299
- "models": {
300
- "providers": {
301
- "anthropic": {
302
- "baseUrl": "http://127.0.0.1:3456",
303
- "apiKey": "dummy",
304
- "models": [
305
- { "id": "claude-sonnet-4-6", "name": "Claude Sonnet 4.6 (Meridian)" },
306
- { "id": "claude-opus-4-6", "name": "Claude Opus 4.6 (Meridian)" }
307
- ]
308
- }
309
- }
310
- }
311
- }
312
- ```
313
-
314
- Then start Meridian with the pi adapter: `MERIDIAN_DEFAULT_AGENT=pi meridian`
315
-
316
296
  ### Any Anthropic-compatible tool
317
297
 
318
298
  ```bash
@@ -331,7 +311,6 @@ export ANTHROPIC_BASE_URL=http://127.0.0.1:3456
331
311
  | [Aider](https://github.com/paul-gauthier/aider) | ✅ Verified | Env vars — file editing, streaming; `--no-stream` broken (litellm bug) |
332
312
  | [Open WebUI](https://github.com/open-webui/open-webui) | ✅ Verified | OpenAI-compatible endpoints — set base URL to `http://127.0.0.1:3456` |
333
313
  | [Pi](https://github.com/mariozechner/pi-coding-agent) | ✅ Verified | models.json config (see above) — requires `MERIDIAN_DEFAULT_AGENT=pi` |
334
- | [OpenClaw](https://github.com/openclaw/openclaw) | ✅ Verified | Provider config (see above) — uses pi adapter via `MERIDIAN_DEFAULT_AGENT=pi` |
335
314
  | [Continue](https://github.com/continuedev/continue) | 🔲 Untested | OpenAI-compatible endpoints should work — set `apiBase` to `http://127.0.0.1:3456` |
336
315
 
337
316
  Tested an agent or built a plugin? [Open an issue](https://github.com/rynfar/meridian/issues) and we'll add it.
@@ -529,7 +508,7 @@ You haven't run `meridian setup`. Without the plugin, OpenCode requests won't ha
529
508
 
530
509
  ## Contributing
531
510
 
532
- 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.
533
512
 
534
513
  ## License
535
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.1",
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",