@rynfar/meridian 1.25.1 → 1.26.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  ---
13
13
 
14
- Meridian turns your Claude Max subscription into a local Anthropic API. Any tool that speaks the Anthropic protocol — OpenCode, Crush, Cline, Aider — connects to Meridian and gets Claude, powered by your existing subscription through the official Claude Code SDK.
14
+ Meridian turns your Claude Max subscription into a local Anthropic API. Any tool that speaks the Anthropic or OpenAI protocol — OpenCode, Crush, Cline, Aider, Open WebUI — connects to Meridian and gets Claude, powered by your existing subscription through the official Claude Code SDK.
15
15
 
16
16
  > [!NOTE]
17
17
  > **Renamed from `opencode-claude-max-proxy`.** If you're upgrading, see [`MIGRATION.md`](MIGRATION.md) for the checklist. Your existing sessions, env vars, and agent configs all continue to work.
@@ -53,6 +53,7 @@ Meridian bridges that gap. It runs locally, accepts standard Anthropic API reque
53
53
  ## Features
54
54
 
55
55
  - **Standard Anthropic API** — drop-in compatible with any tool that supports a custom `base_url`
56
+ - **OpenAI-compatible API** — `/v1/chat/completions` and `/v1/models` for tools that only speak the OpenAI protocol (Open WebUI, Continue, etc.) — no LiteLLM needed
56
57
  - **Session management** — conversations persist across requests, survive compaction and undo, resume after proxy restarts
57
58
  - **Streaming** — full SSE streaming with MCP tool filtering
58
59
  - **Concurrent sessions** — run parent and subagent requests in parallel
@@ -173,6 +174,24 @@ ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://127.0.0.1:3456 \
173
174
 
174
175
  > **Note:** `--no-stream` is incompatible due to a litellm parsing issue — use the default streaming mode.
175
176
 
177
+ ### OpenAI-compatible tools (Open WebUI, Continue, etc.)
178
+
179
+ Meridian speaks the OpenAI protocol natively — no LiteLLM or translation proxy needed.
180
+
181
+ **`POST /v1/chat/completions`** — accepts OpenAI chat format, returns OpenAI completion format (streaming and non-streaming)
182
+
183
+ **`GET /v1/models`** — returns available Claude models in OpenAI format
184
+
185
+ Point any OpenAI-compatible tool at `http://127.0.0.1:3456` with any API key value:
186
+
187
+ ```bash
188
+ # Open WebUI: set OpenAI API base to http://127.0.0.1:3456, API key to any value
189
+ # Continue: set apiBase to http://127.0.0.1:3456 with provider: openai
190
+ # Any OpenAI SDK: set base_url="http://127.0.0.1:3456", api_key="dummy"
191
+ ```
192
+
193
+ > **Note:** Multi-turn conversations work by packing prior turns into the system prompt. Each request is a fresh SDK session — OpenAI clients replay full history themselves and don't use Meridian's session resumption.
194
+
176
195
  ### Any Anthropic-compatible tool
177
196
 
178
197
  ```bash
@@ -189,7 +208,8 @@ export ANTHROPIC_BASE_URL=http://127.0.0.1:3456
189
208
  | [Crush](https://github.com/charmbracelet/crush) | ✅ Verified | Provider config (see above) — full tool support, session resume, headless `crush run` |
190
209
  | [Cline](https://github.com/cline/cline) | ✅ Verified | Config (see above) — full tool support, file read/write/edit, bash, session resume |
191
210
  | [Aider](https://github.com/paul-gauthier/aider) | ✅ Verified | Env vars — file editing, streaming; `--no-stream` broken (litellm bug) |
192
- | [Continue](https://github.com/continuedev/continue) | 🔲 Untested | Should workstandard Anthropic API |
211
+ | [Open WebUI](https://github.com/open-webui/open-webui) | Verified | OpenAI-compatible endpointsset base URL to `http://127.0.0.1:3456` |
212
+ | [Continue](https://github.com/continuedev/continue) | 🔲 Untested | OpenAI-compatible endpoints should work — set `apiBase` to `http://127.0.0.1:3456` |
193
213
 
194
214
  Tested an agent or built a plugin? [Open an issue](https://github.com/rynfar/meridian/issues) and we'll add it.
195
215
 
@@ -209,6 +229,7 @@ src/proxy/
209
229
  ├── errors.ts ← Error classification
210
230
  ├── models.ts ← Model mapping (sonnet/opus/haiku, agentMode)
211
231
  ├── tokenRefresh.ts ← Cross-platform OAuth token refresh
232
+ ├── openai.ts ← OpenAI ↔ Anthropic format translation (pure)
212
233
  ├── setup.ts ← OpenCode plugin configuration
213
234
  ├── session/
214
235
  │ ├── lineage.ts ← Per-message hashing, mutation classification (pure)
@@ -272,6 +293,8 @@ Implement the `AgentAdapter` interface in `src/proxy/adapters/`. See [`adapters/
272
293
  | `GET /` | Landing page |
273
294
  | `POST /v1/messages` | Anthropic Messages API |
274
295
  | `POST /messages` | Alias for `/v1/messages` |
296
+ | `POST /v1/chat/completions` | OpenAI-compatible chat completions |
297
+ | `GET /v1/models` | OpenAI-compatible model list |
275
298
  | `GET /health` | Auth status, mode, plugin status |
276
299
  | `POST /auth/refresh` | Manually refresh the OAuth token |
277
300
  | `GET /telemetry` | Performance dashboard |
@@ -7128,6 +7128,146 @@ function isClosedControllerError(error) {
7128
7128
  return error.message.includes("Controller is already closed");
7129
7129
  }
7130
7130
 
7131
+ // src/proxy/openai.ts
7132
+ function extractOpenAiContent(content) {
7133
+ if (typeof content === "string")
7134
+ return content;
7135
+ return content.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
7136
+ }
7137
+ function translateOpenAiToAnthropic(body) {
7138
+ const messages = body.messages ?? [];
7139
+ if (messages.length === 0)
7140
+ return null;
7141
+ const systemParts = [];
7142
+ const turns = [];
7143
+ for (const msg of messages) {
7144
+ const text = extractOpenAiContent(msg.content ?? "");
7145
+ if (msg.role === "system") {
7146
+ if (text)
7147
+ systemParts.push(text);
7148
+ } else {
7149
+ turns.push({
7150
+ role: msg.role === "assistant" ? "assistant" : "user",
7151
+ content: text
7152
+ });
7153
+ }
7154
+ }
7155
+ let systemPrompt = systemParts.join(`
7156
+ `);
7157
+ let messagesToSend = turns;
7158
+ if (turns.length > 1) {
7159
+ const history = turns.slice(0, -1).map((m) => `${m.role}: ${m.content}`).join(`
7160
+ `);
7161
+ const historyBlock = `<conversation_history>
7162
+ ${history}
7163
+ </conversation_history>
7164
+
7165
+ ` + `Continue this conversation naturally. Respond to the user's latest message.`;
7166
+ systemPrompt = systemPrompt ? `${systemPrompt}
7167
+
7168
+ ${historyBlock}` : historyBlock;
7169
+ messagesToSend = turns.slice(-1);
7170
+ }
7171
+ const result = {
7172
+ model: body.model ?? "claude-sonnet-4-6",
7173
+ messages: messagesToSend,
7174
+ max_tokens: body.max_tokens ?? body.max_completion_tokens ?? 8192,
7175
+ stream: body.stream ?? false
7176
+ };
7177
+ if (systemPrompt)
7178
+ result.system = systemPrompt;
7179
+ if (body.temperature !== undefined)
7180
+ result.temperature = body.temperature;
7181
+ if (body.top_p !== undefined)
7182
+ result.top_p = body.top_p;
7183
+ return result;
7184
+ }
7185
+ function toFinishReason(stopReason) {
7186
+ if (stopReason === "max_tokens")
7187
+ return "length";
7188
+ return "stop";
7189
+ }
7190
+ function translateAnthropicToOpenAi(response, completionId, model, created) {
7191
+ const content = (response.content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
7192
+ const promptTokens = response.usage?.input_tokens ?? 0;
7193
+ const completionTokens = response.usage?.output_tokens ?? 0;
7194
+ return {
7195
+ id: completionId,
7196
+ object: "chat.completion",
7197
+ created,
7198
+ model,
7199
+ choices: [{
7200
+ index: 0,
7201
+ message: { role: "assistant", content },
7202
+ finish_reason: toFinishReason(response.stop_reason)
7203
+ }],
7204
+ usage: {
7205
+ prompt_tokens: promptTokens,
7206
+ completion_tokens: completionTokens,
7207
+ total_tokens: promptTokens + completionTokens
7208
+ }
7209
+ };
7210
+ }
7211
+ function translateAnthropicSseEvent(event, completionId, model, created) {
7212
+ if (event.type === "message_start") {
7213
+ return {
7214
+ id: completionId,
7215
+ object: "chat.completion.chunk",
7216
+ created,
7217
+ model,
7218
+ choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }]
7219
+ };
7220
+ }
7221
+ if (event.type === "content_block_delta" && event.delta?.type === "text_delta" && typeof event.delta.text === "string") {
7222
+ return {
7223
+ id: completionId,
7224
+ object: "chat.completion.chunk",
7225
+ created,
7226
+ model,
7227
+ choices: [{ index: 0, delta: { content: event.delta.text }, finish_reason: null }]
7228
+ };
7229
+ }
7230
+ if (event.type === "message_delta" && event.delta?.stop_reason) {
7231
+ return {
7232
+ id: completionId,
7233
+ object: "chat.completion.chunk",
7234
+ created,
7235
+ model,
7236
+ choices: [{ index: 0, delta: {}, finish_reason: toFinishReason(event.delta.stop_reason) }]
7237
+ };
7238
+ }
7239
+ return null;
7240
+ }
7241
+ function buildModelList(isMaxSubscription, now = Math.floor(Date.now() / 1000)) {
7242
+ const extendedContext = isMaxSubscription ? 1e6 : 200000;
7243
+ return [
7244
+ {
7245
+ id: "claude-sonnet-4-6",
7246
+ object: "model",
7247
+ created: now,
7248
+ owned_by: "anthropic",
7249
+ display_name: "Claude Sonnet 4.6",
7250
+ context_window: extendedContext
7251
+ },
7252
+ {
7253
+ id: "claude-opus-4-6",
7254
+ object: "model",
7255
+ created: now,
7256
+ owned_by: "anthropic",
7257
+ display_name: "Claude Opus 4.6",
7258
+ context_window: extendedContext
7259
+ },
7260
+ {
7261
+ id: "claude-haiku-4-5-20251001",
7262
+ object: "model",
7263
+ created: now,
7264
+ owned_by: "anthropic",
7265
+ display_name: "Claude Haiku 4.5",
7266
+ context_window: 200000
7267
+ }
7268
+ ];
7269
+ }
7270
+
7131
7271
  // src/proxy/messages.ts
7132
7272
  function stripCacheControlForHashing(obj) {
7133
7273
  if (!obj || typeof obj !== "object")
@@ -13465,7 +13605,11 @@ function buildQueryOptions(ctx) {
13465
13605
  undoRollbackUuid,
13466
13606
  sdkHooks,
13467
13607
  adapter,
13468
- onStderr
13608
+ onStderr,
13609
+ effort,
13610
+ thinking,
13611
+ taskBudget,
13612
+ betas
13469
13613
  } = ctx;
13470
13614
  const blockedTools = [...adapter.getBlockedBuiltinTools(), ...adapter.getAgentIncompatibleTools()];
13471
13615
  const mcpServerName = adapter.getMcpServerName();
@@ -13504,7 +13648,11 @@ function buildQueryOptions(ctx) {
13504
13648
  ...Object.keys(sdkAgents).length > 0 ? { agents: sdkAgents } : {},
13505
13649
  ...resumeSessionId ? { resume: resumeSessionId } : {},
13506
13650
  ...isUndo ? { forkSession: true, ...undoRollbackUuid ? { resumeSessionAt: undoRollbackUuid } : {} } : {},
13507
- ...sdkHooks ? { hooks: sdkHooks } : {}
13651
+ ...sdkHooks ? { hooks: sdkHooks } : {},
13652
+ ...effort ? { effort } : {},
13653
+ ...thinking ? { thinking } : {},
13654
+ ...taskBudget ? { taskBudget } : {},
13655
+ ...betas && betas.length > 0 ? { betas } : {}
13508
13656
  }
13509
13657
  };
13510
13658
  }
@@ -13784,7 +13932,19 @@ function lookupSharedSession(key) {
13784
13932
  const store = readStore();
13785
13933
  return store[key];
13786
13934
  }
13787
- function storeSharedSession(key, claudeSessionId, messageCount, lineageHash, messageHashes, sdkMessageUuids) {
13935
+ function lookupSharedSessionByClaudeId(claudeSessionId) {
13936
+ const sessions = Object.values(readStore());
13937
+ let newest;
13938
+ for (const session of sessions) {
13939
+ if (session.claudeSessionId !== claudeSessionId)
13940
+ continue;
13941
+ if (!newest || session.lastUsedAt > newest.lastUsedAt) {
13942
+ newest = session;
13943
+ }
13944
+ }
13945
+ return newest;
13946
+ }
13947
+ function storeSharedSession(key, claudeSessionId, messageCount, lineageHash, messageHashes, sdkMessageUuids, contextUsage) {
13788
13948
  const path3 = getStorePath();
13789
13949
  const lockPath = `${path3}.lock`;
13790
13950
  const hasLock = skipLocking ? false : acquireLock(lockPath);
@@ -13801,7 +13961,8 @@ function storeSharedSession(key, claudeSessionId, messageCount, lineageHash, mes
13801
13961
  messageCount: messageCount ?? existing?.messageCount ?? 0,
13802
13962
  lineageHash: lineageHash ?? existing?.lineageHash,
13803
13963
  messageHashes: messageHashes ?? existing?.messageHashes,
13804
- sdkMessageUuids: sdkMessageUuids ?? existing?.sdkMessageUuids
13964
+ sdkMessageUuids: sdkMessageUuids ?? existing?.sdkMessageUuids,
13965
+ contextUsage: contextUsage ?? existing?.contextUsage
13805
13966
  };
13806
13967
  const maxEntries = getMaxStoredSessions();
13807
13968
  const keys = Object.keys(store);
@@ -13947,7 +14108,8 @@ function lookupSession(sessionId, messages, workingDirectory) {
13947
14108
  messageCount: shared.messageCount || 0,
13948
14109
  lineageHash: shared.lineageHash || "",
13949
14110
  messageHashes: shared.messageHashes,
13950
- sdkMessageUuids: shared.sdkMessageUuids
14111
+ sdkMessageUuids: shared.sdkMessageUuids,
14112
+ contextUsage: shared.contextUsage
13951
14113
  };
13952
14114
  const result = verifyLineage(state, messages, sessionId, sessionCache);
13953
14115
  if (result.type === "continuation" || result.type === "compaction") {
@@ -13974,7 +14136,8 @@ function lookupSession(sessionId, messages, workingDirectory) {
13974
14136
  messageCount: shared.messageCount || 0,
13975
14137
  lineageHash: shared.lineageHash || "",
13976
14138
  messageHashes: shared.messageHashes,
13977
- sdkMessageUuids: shared.sdkMessageUuids
14139
+ sdkMessageUuids: shared.sdkMessageUuids,
14140
+ contextUsage: shared.contextUsage
13978
14141
  };
13979
14142
  const result = verifyLineage(state, messages, fp, fingerprintCache);
13980
14143
  if (result.type === "continuation" || result.type === "compaction") {
@@ -13985,7 +14148,34 @@ function lookupSession(sessionId, messages, workingDirectory) {
13985
14148
  }
13986
14149
  return { type: "diverged" };
13987
14150
  }
13988
- function storeSession(sessionId, messages, claudeSessionId, workingDirectory, sdkMessageUuids) {
14151
+ function getSessionByClaudeId(claudeSessionId) {
14152
+ let newest;
14153
+ const consider = (state) => {
14154
+ if (!state || state.claudeSessionId !== claudeSessionId)
14155
+ return;
14156
+ if (!newest || state.lastAccess > newest.lastAccess) {
14157
+ newest = state;
14158
+ }
14159
+ };
14160
+ for (const state of sessionCache.values())
14161
+ consider(state);
14162
+ for (const state of fingerprintCache.values())
14163
+ consider(state);
14164
+ const shared = lookupSharedSessionByClaudeId(claudeSessionId);
14165
+ if (shared) {
14166
+ consider({
14167
+ claudeSessionId: shared.claudeSessionId,
14168
+ lastAccess: shared.lastUsedAt,
14169
+ messageCount: shared.messageCount || 0,
14170
+ lineageHash: shared.lineageHash || "",
14171
+ messageHashes: shared.messageHashes,
14172
+ sdkMessageUuids: shared.sdkMessageUuids,
14173
+ contextUsage: shared.contextUsage
14174
+ });
14175
+ }
14176
+ return newest;
14177
+ }
14178
+ function storeSession(sessionId, messages, claudeSessionId, workingDirectory, sdkMessageUuids, contextUsage) {
13989
14179
  if (!claudeSessionId)
13990
14180
  return;
13991
14181
  const lineageHash = computeLineageHash(messages);
@@ -13996,7 +14186,8 @@ function storeSession(sessionId, messages, claudeSessionId, workingDirectory, sd
13996
14186
  messageCount: messages?.length || 0,
13997
14187
  lineageHash,
13998
14188
  messageHashes,
13999
- sdkMessageUuids
14189
+ sdkMessageUuids,
14190
+ ...contextUsage ? { contextUsage } : {}
14000
14191
  };
14001
14192
  if (sessionId)
14002
14193
  sessionCache.set(sessionId, state);
@@ -14004,8 +14195,9 @@ function storeSession(sessionId, messages, claudeSessionId, workingDirectory, sd
14004
14195
  if (fp)
14005
14196
  fingerprintCache.set(fp, state);
14006
14197
  const key = sessionId || fp;
14007
- if (key)
14008
- storeSharedSession(key, claudeSessionId, state.messageCount, lineageHash, messageHashes, sdkMessageUuids);
14198
+ if (key) {
14199
+ storeSharedSession(key, claudeSessionId, state.messageCount, lineageHash, messageHashes, sdkMessageUuids, contextUsage);
14200
+ }
14009
14201
  }
14010
14202
 
14011
14203
  // src/proxy/server.ts
@@ -14083,6 +14275,16 @@ function buildFreshPrompt(messages, stripCacheControl) {
14083
14275
 
14084
14276
  `) || "";
14085
14277
  }
14278
+ function logUsage(requestId, usage) {
14279
+ const fmt = (n) => n > 1000 ? `${Math.round(n / 1000)}k` : String(n);
14280
+ const parts = [
14281
+ `input=${fmt(usage.input_tokens ?? 0)}`,
14282
+ `output=${fmt(usage.output_tokens ?? 0)}`,
14283
+ ...usage.cache_read_input_tokens ? [`cache_read=${fmt(usage.cache_read_input_tokens)}`] : [],
14284
+ ...usage.cache_creation_input_tokens ? [`cache_write=${fmt(usage.cache_creation_input_tokens)}`] : []
14285
+ ];
14286
+ console.error(`[PROXY] ${requestId} usage: ${parts.join(" ")}`);
14287
+ }
14086
14288
  function createProxyServer(config = {}) {
14087
14289
  const finalConfig = { ...DEFAULT_PROXY_CONFIG, ...config };
14088
14290
  const app = new Hono2;
@@ -14094,7 +14296,7 @@ function createProxyServer(config = {}) {
14094
14296
  status: "ok",
14095
14297
  service: "meridian",
14096
14298
  format: "anthropic",
14097
- endpoints: ["/v1/messages", "/messages", "/telemetry", "/health"]
14299
+ endpoints: ["/v1/messages", "/messages", "/v1/chat/completions", "/v1/models", "/telemetry", "/health"]
14098
14300
  });
14099
14301
  }
14100
14302
  return c.html(landingHtml);
@@ -14170,6 +14372,22 @@ function createProxyServer(config = {}) {
14170
14372
  `);
14171
14373
  }
14172
14374
  }
14375
+ const effortHeader = c.req.header("x-opencode-effort");
14376
+ const thinkingHeader = c.req.header("x-opencode-thinking");
14377
+ const taskBudgetHeader = c.req.header("x-opencode-task-budget");
14378
+ const betaHeader = c.req.header("anthropic-beta");
14379
+ const effort = effortHeader || body.effort || undefined;
14380
+ let thinking = body.thinking || undefined;
14381
+ if (thinkingHeader !== undefined) {
14382
+ try {
14383
+ thinking = JSON.parse(thinkingHeader);
14384
+ } catch (e) {
14385
+ console.error(`[PROXY] ${requestMeta.requestId} ignoring malformed x-opencode-thinking header: ${e instanceof Error ? e.message : String(e)}`);
14386
+ }
14387
+ }
14388
+ const parsedBudget = taskBudgetHeader ? Number.parseInt(taskBudgetHeader, 10) : NaN;
14389
+ const taskBudget = Number.isFinite(parsedBudget) ? { total: parsedBudget } : body.task_budget ? { total: body.task_budget.total ?? body.task_budget } : undefined;
14390
+ const betas = betaHeader ? betaHeader.split(",").map((b) => b.trim()).filter(Boolean) : undefined;
14173
14391
  const agentSessionId = adapter.getSessionId(c);
14174
14392
  const lineageResult = lookupSession(agentSessionId, body.messages || [], workingDirectory);
14175
14393
  const isResume = lineageResult.type === "continuation" || lineageResult.type === "compaction";
@@ -14343,6 +14561,7 @@ function createProxyServer(config = {}) {
14343
14561
  while (sdkUuidMap.length < allMessages.length)
14344
14562
  sdkUuidMap.push(null);
14345
14563
  claudeLog("upstream.start", { mode: "non_stream", model });
14564
+ let lastUsage;
14346
14565
  try {
14347
14566
  if (!claudeExecutable) {
14348
14567
  claudeExecutable = await resolveClaudeExecutableAsync();
@@ -14371,7 +14590,11 @@ function createProxyServer(config = {}) {
14371
14590
  undoRollbackUuid,
14372
14591
  sdkHooks,
14373
14592
  adapter,
14374
- onStderr
14593
+ onStderr,
14594
+ effort,
14595
+ thinking,
14596
+ taskBudget,
14597
+ betas
14375
14598
  }))) {
14376
14599
  if (event.type === "assistant" && !event.error) {
14377
14600
  didYieldContent = true;
@@ -14410,7 +14633,11 @@ function createProxyServer(config = {}) {
14410
14633
  undoRollbackUuid: undefined,
14411
14634
  sdkHooks,
14412
14635
  adapter,
14413
- onStderr
14636
+ onStderr,
14637
+ effort,
14638
+ thinking,
14639
+ taskBudget,
14640
+ betas
14414
14641
  }));
14415
14642
  return;
14416
14643
  }
@@ -14492,6 +14719,9 @@ function createProxyServer(config = {}) {
14492
14719
  }
14493
14720
  contentBlocks.push(b);
14494
14721
  }
14722
+ const msgUsage = message.message.usage;
14723
+ if (msgUsage)
14724
+ lastUsage = { ...lastUsage, ...msgUsage };
14495
14725
  }
14496
14726
  }
14497
14727
  claudeLog("upstream.completed", {
@@ -14500,6 +14730,8 @@ function createProxyServer(config = {}) {
14500
14730
  assistantMessages,
14501
14731
  durationMs: Date.now() - upstreamStartAt
14502
14732
  });
14733
+ if (lastUsage)
14734
+ logUsage(requestMeta.requestId, lastUsage);
14503
14735
  } catch (error) {
14504
14736
  const stderrOutput = stderrLines.join(`
14505
14737
  `).trim();
@@ -14584,7 +14816,7 @@ Subprocess stderr: ${stderrOutput}`;
14584
14816
  error: null
14585
14817
  });
14586
14818
  if (currentSessionId) {
14587
- storeSession(agentSessionId, body.messages || [], currentSessionId, workingDirectory, sdkUuidMap);
14819
+ storeSession(agentSessionId, body.messages || [], currentSessionId, workingDirectory, sdkUuidMap, lastUsage);
14588
14820
  }
14589
14821
  const responseSessionId = currentSessionId || resumeSessionId || `session_${Date.now()}`;
14590
14822
  return new Response(JSON.stringify({
@@ -14638,6 +14870,7 @@ Subprocess stderr: ${stderrOutput}`;
14638
14870
  while (sdkUuidMap.length < allMessages.length)
14639
14871
  sdkUuidMap.push(null);
14640
14872
  let messageStartEmitted = false;
14873
+ let lastUsage;
14641
14874
  try {
14642
14875
  let currentSessionId;
14643
14876
  const MAX_RATE_LIMIT_RETRIES = 2;
@@ -14664,7 +14897,11 @@ Subprocess stderr: ${stderrOutput}`;
14664
14897
  undoRollbackUuid,
14665
14898
  sdkHooks,
14666
14899
  adapter,
14667
- onStderr
14900
+ onStderr,
14901
+ effort,
14902
+ thinking,
14903
+ taskBudget,
14904
+ betas
14668
14905
  }))) {
14669
14906
  if (event.type === "stream_event") {
14670
14907
  didYieldClientEvent = true;
@@ -14703,7 +14940,11 @@ Subprocess stderr: ${stderrOutput}`;
14703
14940
  undoRollbackUuid: undefined,
14704
14941
  sdkHooks,
14705
14942
  adapter,
14706
- onStderr
14943
+ onStderr,
14944
+ effort,
14945
+ thinking,
14946
+ taskBudget,
14947
+ betas
14707
14948
  }));
14708
14949
  return;
14709
14950
  }
@@ -14813,6 +15054,9 @@ Subprocess stderr: ${stderrOutput}`;
14813
15054
  if (eventType === "message_start") {
14814
15055
  skipBlockIndices.clear();
14815
15056
  sdkToClientIndex.clear();
15057
+ const startUsage = event.message?.usage;
15058
+ if (startUsage)
15059
+ lastUsage = { ...lastUsage, ...startUsage };
14816
15060
  if (messageStartEmitted) {
14817
15061
  continue;
14818
15062
  }
@@ -14845,6 +15089,9 @@ Subprocess stderr: ${stderrOutput}`;
14845
15089
  event.index = sdkToClientIndex.get(eventIndex);
14846
15090
  }
14847
15091
  if (eventType === "message_delta") {
15092
+ const deltaUsage = event.usage;
15093
+ if (deltaUsage)
15094
+ lastUsage = { ...lastUsage, ...deltaUsage };
14848
15095
  const stopReason = event.delta?.stop_reason;
14849
15096
  if (stopReason === "tool_use" && skipBlockIndices.size > 0) {
14850
15097
  continue;
@@ -14886,8 +15133,10 @@ data: ${JSON.stringify({ type: "message_stop" })}
14886
15133
  eventsForwarded,
14887
15134
  textEventsForwarded
14888
15135
  });
15136
+ if (lastUsage)
15137
+ logUsage(requestMeta.requestId, lastUsage);
14889
15138
  if (currentSessionId) {
14890
- storeSession(agentSessionId, body.messages || [], currentSessionId, workingDirectory, sdkUuidMap);
15139
+ storeSession(agentSessionId, body.messages || [], currentSessionId, workingDirectory, sdkUuidMap, lastUsage);
14891
15140
  }
14892
15141
  if (!streamClosed) {
14893
15142
  const unseenToolUses = capturedToolUses.filter((tu) => !streamedToolUseIds.has(tu.id));
@@ -15180,6 +15429,105 @@ data: ${JSON.stringify({
15180
15429
  }
15181
15430
  return c.json({ success: false, message: "Token refresh failed. If the problem persists, run 'claude login'." }, 500);
15182
15431
  });
15432
+ app.post("/v1/chat/completions", async (c) => {
15433
+ const rawBody = await c.req.json();
15434
+ const anthropicBody = translateOpenAiToAnthropic(rawBody);
15435
+ if (!anthropicBody) {
15436
+ return c.json({ type: "error", error: { type: "invalid_request_error", message: "messages: Field required" } }, 400);
15437
+ }
15438
+ const internalReq = new Request("http://internal/v1/messages", {
15439
+ method: "POST",
15440
+ headers: { "Content-Type": "application/json" },
15441
+ body: JSON.stringify(anthropicBody)
15442
+ });
15443
+ const internalRes = await app.fetch(internalReq);
15444
+ if (!internalRes.ok) {
15445
+ const errBody = await internalRes.text();
15446
+ return c.json({ type: "error", error: { type: "upstream_error", message: errBody } }, internalRes.status);
15447
+ }
15448
+ const completionId = `chatcmpl-${randomUUID()}`;
15449
+ const created = Math.floor(Date.now() / 1000);
15450
+ const model = typeof rawBody.model === "string" && rawBody.model ? rawBody.model : "claude-sonnet-4-6";
15451
+ if (!anthropicBody.stream) {
15452
+ const anthropicRes = await internalRes.json();
15453
+ return c.json(translateAnthropicToOpenAi(anthropicRes, completionId, model, created));
15454
+ }
15455
+ const encoder = new TextEncoder;
15456
+ const readable = new ReadableStream({
15457
+ async start(controller) {
15458
+ const reader = internalRes.body?.getReader();
15459
+ if (!reader) {
15460
+ controller.close();
15461
+ return;
15462
+ }
15463
+ const decoder = new TextDecoder;
15464
+ let buffer = "";
15465
+ let streamError = null;
15466
+ try {
15467
+ while (true) {
15468
+ const { done, value } = await reader.read();
15469
+ if (done)
15470
+ break;
15471
+ buffer += decoder.decode(value, { stream: true });
15472
+ const lines = buffer.split(`
15473
+ `);
15474
+ buffer = lines.pop() ?? "";
15475
+ for (const line of lines) {
15476
+ if (!line.startsWith("data: "))
15477
+ continue;
15478
+ const dataStr = line.slice(6).trim();
15479
+ if (!dataStr)
15480
+ continue;
15481
+ let event;
15482
+ try {
15483
+ event = JSON.parse(dataStr);
15484
+ } catch {
15485
+ continue;
15486
+ }
15487
+ if (typeof event.type !== "string")
15488
+ continue;
15489
+ const chunk = translateAnthropicSseEvent(event, completionId, model, created);
15490
+ if (chunk)
15491
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}
15492
+
15493
+ `));
15494
+ }
15495
+ }
15496
+ } catch (err) {
15497
+ streamError = err instanceof Error ? err : new Error(String(err));
15498
+ } finally {
15499
+ if (!streamError)
15500
+ controller.enqueue(encoder.encode(`data: [DONE]
15501
+
15502
+ `));
15503
+ controller.close();
15504
+ }
15505
+ }
15506
+ });
15507
+ return new Response(readable, {
15508
+ headers: {
15509
+ "Content-Type": "text/event-stream",
15510
+ "Cache-Control": "no-cache",
15511
+ Connection: "keep-alive"
15512
+ }
15513
+ });
15514
+ });
15515
+ app.get("/v1/models", async (c) => {
15516
+ const authStatus = await getClaudeAuthStatusAsync();
15517
+ const isMax = authStatus?.subscriptionType === "max";
15518
+ return c.json({ object: "list", data: buildModelList(isMax) });
15519
+ });
15520
+ app.get("/v1/sessions/:claudeSessionId/context-usage", (c) => {
15521
+ const claudeSessionId = c.req.param("claudeSessionId");
15522
+ const session = getSessionByClaudeId(claudeSessionId);
15523
+ if (!session) {
15524
+ return c.json({ error: "Session not found" }, 404);
15525
+ }
15526
+ if (!session.contextUsage) {
15527
+ return c.json({ error: "No usage data available for this session" }, 404);
15528
+ }
15529
+ return c.json({ session_id: claudeSessionId, context_usage: session.contextUsage });
15530
+ });
15183
15531
  app.all("*", (c) => {
15184
15532
  console.error(`[PROXY] UNHANDLED ${c.req.method} ${c.req.url}`);
15185
15533
  return c.json({ error: { type: "not_found", message: `Endpoint not supported: ${c.req.method} ${new URL(c.req.url).pathname}` } }, 404);
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startProxyServer
4
- } from "./cli-s6f9jefk.js";
4
+ } from "./cli-yve9q0a0.js";
5
5
  import"./cli-rtab0qa6.js";
6
6
  import"./cli-m9pfb7h9.js";
7
7
  import {
@@ -0,0 +1,142 @@
1
+ /**
2
+ * OpenAI ↔ Anthropic format translation.
3
+ *
4
+ * Pure functions — no I/O, no side effects. Used by the /v1/chat/completions
5
+ * and /v1/models routes in server.ts.
6
+ *
7
+ * Design note: OpenAI clients always send the full conversation history on
8
+ * every request. Feeding that directly into Meridian's session system would
9
+ * classify every turn as "undo" or "diverged" (since the message list keeps
10
+ * changing). Instead:
11
+ * 1. The last user message becomes the actual SDK request
12
+ * 2. Prior turns are packed into a <conversation_history> block in the
13
+ * system prompt so Claude has context
14
+ * 3. Each chat completions request gets a fresh SDK session
15
+ * This is intentional — OpenAI-format clients replay full history themselves
16
+ * and don't benefit from Meridian's session resumption.
17
+ */
18
+ export type OpenAiRole = "system" | "user" | "assistant";
19
+ export interface OpenAiContentPart {
20
+ type: string;
21
+ text?: string;
22
+ }
23
+ export interface OpenAiMessage {
24
+ role: OpenAiRole;
25
+ content: string | OpenAiContentPart[];
26
+ }
27
+ export interface OpenAiChatRequest {
28
+ model?: string;
29
+ messages?: OpenAiMessage[];
30
+ stream?: boolean;
31
+ max_tokens?: number;
32
+ max_completion_tokens?: number;
33
+ temperature?: number;
34
+ top_p?: number;
35
+ }
36
+ export interface AnthropicMessage {
37
+ role: "user" | "assistant";
38
+ content: string;
39
+ }
40
+ export interface AnthropicRequestBody {
41
+ model: string;
42
+ messages: AnthropicMessage[];
43
+ max_tokens: number;
44
+ stream: boolean;
45
+ system?: string;
46
+ temperature?: number;
47
+ top_p?: number;
48
+ }
49
+ export interface AnthropicUsage {
50
+ input_tokens?: number;
51
+ output_tokens?: number;
52
+ }
53
+ export interface AnthropicContentBlock {
54
+ type: string;
55
+ text?: string;
56
+ }
57
+ export interface AnthropicResponse {
58
+ content?: AnthropicContentBlock[];
59
+ stop_reason?: string;
60
+ usage?: AnthropicUsage;
61
+ }
62
+ export interface OpenAiStreamChunk {
63
+ id: string;
64
+ object: "chat.completion.chunk";
65
+ created: number;
66
+ model: string;
67
+ choices: Array<{
68
+ index: 0;
69
+ delta: {
70
+ role?: "assistant";
71
+ content?: string;
72
+ };
73
+ finish_reason: "stop" | "length" | null;
74
+ }>;
75
+ }
76
+ export interface OpenAiCompletion {
77
+ id: string;
78
+ object: "chat.completion";
79
+ created: number;
80
+ model: string;
81
+ choices: Array<{
82
+ index: 0;
83
+ message: {
84
+ role: "assistant";
85
+ content: string;
86
+ };
87
+ finish_reason: "stop" | "length";
88
+ }>;
89
+ usage: {
90
+ prompt_tokens: number;
91
+ completion_tokens: number;
92
+ total_tokens: number;
93
+ };
94
+ }
95
+ export interface OpenAiModel {
96
+ id: string;
97
+ object: "model";
98
+ created: number;
99
+ owned_by: string;
100
+ display_name: string;
101
+ context_window: number;
102
+ }
103
+ /**
104
+ * Normalise an OpenAI message content field to a plain string.
105
+ * Handles both string content and structured content arrays.
106
+ */
107
+ export declare function extractOpenAiContent(content: string | OpenAiContentPart[]): string;
108
+ /**
109
+ * Translate an OpenAI /v1/chat/completions request body into an Anthropic
110
+ * /v1/messages request body.
111
+ *
112
+ * Returns null if the request has no messages (caller should return 400).
113
+ */
114
+ export declare function translateOpenAiToAnthropic(body: OpenAiChatRequest): AnthropicRequestBody | null;
115
+ /**
116
+ * Translate a complete Anthropic /v1/messages response to OpenAI format.
117
+ * Thinking blocks are filtered out — only text blocks are included.
118
+ */
119
+ export declare function translateAnthropicToOpenAi(response: AnthropicResponse, completionId: string, model: string, created: number): OpenAiCompletion;
120
+ interface AnthropicSseEvent {
121
+ type: string;
122
+ delta?: {
123
+ type?: string;
124
+ text?: string;
125
+ stop_reason?: string;
126
+ };
127
+ message?: {
128
+ id?: string;
129
+ };
130
+ }
131
+ /**
132
+ * Translate one parsed Anthropic SSE event into an OpenAI stream chunk.
133
+ * Returns null for events that should be skipped (pings, block starts, etc).
134
+ */
135
+ export declare function translateAnthropicSseEvent(event: AnthropicSseEvent, completionId: string, model: string, created: number): OpenAiStreamChunk | null;
136
+ /**
137
+ * Return the static list of available Claude models in OpenAI format.
138
+ * Context windows reflect subscription capabilities.
139
+ */
140
+ export declare function buildModelList(isMaxSubscription: boolean, now?: number): OpenAiModel[];
141
+ export {};
142
+ //# sourceMappingURL=openai.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../src/proxy/openai.ts"],"names":[],"mappings":"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"}
@@ -5,6 +5,7 @@
5
5
  * between the streaming and non-streaming paths in server.ts.
6
6
  */
7
7
  import type { AgentAdapter } from "./adapter";
8
+ import type { Options } from "@anthropic-ai/claude-agent-sdk";
8
9
  import { createPassthroughMcpServer } from "./passthroughTools";
9
10
  export interface QueryContext {
10
11
  /** The prompt to send (text or async iterable for multimodal) */
@@ -39,73 +40,32 @@ export interface QueryContext {
39
40
  adapter: AgentAdapter;
40
41
  /** Callback to receive stderr lines from the Claude subprocess */
41
42
  onStderr?: (line: string) => void;
43
+ /** Effort level — controls thinking depth (low/medium/high/max) */
44
+ effort?: 'low' | 'medium' | 'high' | 'max';
45
+ /** Thinking configuration — adaptive, enabled with budget, or disabled */
46
+ thinking?: {
47
+ type: 'adaptive';
48
+ } | {
49
+ type: 'enabled';
50
+ budgetTokens?: number;
51
+ } | {
52
+ type: 'disabled';
53
+ };
54
+ /** API-side task budget in tokens — model paces tool use within this limit */
55
+ taskBudget?: {
56
+ total: number;
57
+ };
58
+ /** Beta features to enable */
59
+ betas?: string[];
42
60
  }
43
61
  /**
44
62
  * Build the options object for the Claude Agent SDK query() call.
45
63
  * This is called identically from both streaming and non-streaming paths,
46
64
  * with the only difference being `includePartialMessages` for streaming.
47
65
  */
48
- export declare function buildQueryOptions(ctx: QueryContext): {
49
- prompt: string | AsyncIterable<any>;
50
- options: {
51
- hooks?: any;
52
- resumeSessionAt?: string | undefined;
53
- forkSession?: boolean | undefined;
54
- resume?: string | undefined;
55
- agents?: Record<string, any> | undefined;
56
- env: {
57
- ENABLE_CLAUDEAI_MCP_SERVERS?: string | undefined;
58
- ENABLE_TOOL_SEARCH: string;
59
- };
60
- stderr?: ((line: string) => void) | undefined;
61
- plugins: never[];
62
- allowedTools?: string[] | undefined;
63
- mcpServers?: {
64
- oc: import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
65
- } | undefined;
66
- disallowedTools: string[];
67
- systemPrompt?: string | {
68
- type: "preset";
69
- preset: "claude_code";
70
- append: string;
71
- } | undefined;
72
- permissionMode: "bypassPermissions";
73
- allowDangerouslySkipPermissions: boolean;
74
- includePartialMessages?: boolean | undefined;
75
- maxTurns: number;
76
- cwd: string;
77
- model: string;
78
- pathToClaudeCodeExecutable: string;
79
- } | {
80
- hooks?: any;
81
- resumeSessionAt?: string | undefined;
82
- forkSession?: boolean | undefined;
83
- resume?: string | undefined;
84
- agents?: Record<string, any> | undefined;
85
- env: {
86
- ENABLE_CLAUDEAI_MCP_SERVERS?: string | undefined;
87
- ENABLE_TOOL_SEARCH: string;
88
- };
89
- stderr?: ((line: string) => void) | undefined;
90
- plugins: never[];
91
- disallowedTools: string[];
92
- allowedTools: string[];
93
- mcpServers: {
94
- [x: string]: import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
95
- oc?: undefined;
96
- };
97
- systemPrompt?: string | {
98
- type: "preset";
99
- preset: "claude_code";
100
- append: string;
101
- } | undefined;
102
- permissionMode: "bypassPermissions";
103
- allowDangerouslySkipPermissions: boolean;
104
- includePartialMessages?: boolean | undefined;
105
- maxTurns: number;
106
- cwd: string;
107
- model: string;
108
- pathToClaudeCodeExecutable: string;
109
- };
110
- };
66
+ export interface BuildQueryResult {
67
+ prompt: QueryContext["prompt"];
68
+ options: Options;
69
+ }
70
+ export declare function buildQueryOptions(ctx: QueryContext): BuildQueryResult;
111
71
  //# sourceMappingURL=query.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/proxy/query.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAE7C,OAAO,EAAE,0BAA0B,EAAwB,MAAM,oBAAoB,CAAA;AAErF,MAAM,WAAW,YAAY;IAC3B,iEAAiE;IACjE,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,CAAA;IACnC,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAA;IACb,+BAA+B;IAC/B,gBAAgB,EAAE,MAAM,CAAA;IACxB,yCAAyC;IACzC,aAAa,EAAE,MAAM,CAAA;IACrB,gCAAgC;IAChC,gBAAgB,EAAE,MAAM,CAAA;IACxB,0CAA0C;IAC1C,WAAW,EAAE,OAAO,CAAA;IACpB,0CAA0C;IAC1C,MAAM,EAAE,OAAO,CAAA;IACf,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC9B,mEAAmE;IACnE,cAAc,CAAC,EAAE,UAAU,CAAC,OAAO,0BAA0B,CAAC,CAAA;IAC9D,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;IAC5C,0DAA0D;IAC1D,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,wCAAwC;IACxC,MAAM,EAAE,OAAO,CAAA;IACf,8CAA8C;IAC9C,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,GAAG,CAAA;IACd,qDAAqD;IACrD,OAAO,EAAE,YAAY,CAAA;IACrB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAClC;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,YAAY;;;;;;;;;;;;yBAR/B,MAAM,KAAK,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBAAf,MAAM,KAAK,IAAI;;;;;;;;;;;;;;;;;;;;;EAiElC"}
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/proxy/query.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAW,MAAM,gCAAgC,CAAA;AAEtE,OAAO,EAAE,0BAA0B,EAAwB,MAAM,oBAAoB,CAAA;AAErF,MAAM,WAAW,YAAY;IAC3B,iEAAiE;IACjE,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,CAAA;IACnC,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAA;IACb,+BAA+B;IAC/B,gBAAgB,EAAE,MAAM,CAAA;IACxB,yCAAyC;IACzC,aAAa,EAAE,MAAM,CAAA;IACrB,gCAAgC;IAChC,gBAAgB,EAAE,MAAM,CAAA;IACxB,0CAA0C;IAC1C,WAAW,EAAE,OAAO,CAAA;IACpB,0CAA0C;IAC1C,MAAM,EAAE,OAAO,CAAA;IACf,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC9B,mEAAmE;IACnE,cAAc,CAAC,EAAE,UAAU,CAAC,OAAO,0BAA0B,CAAC,CAAA;IAC9D,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;IAC5C,0DAA0D;IAC1D,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,wCAAwC;IACxC,MAAM,EAAE,OAAO,CAAA;IACf,8CAA8C;IAC9C,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,GAAG,CAAA;IACd,qDAAqD;IACrD,OAAO,EAAE,YAAY,CAAA;IACrB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,mEAAmE;IACnE,MAAM,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,KAAK,CAAA;IAC1C,0EAA0E;IAC1E,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,UAAU,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,UAAU,CAAA;KAAE,CAAA;IACnG,8EAA8E;IAC9E,UAAU,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IAC9B,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAA;IAC9B,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,YAAY,GAAG,gBAAgB,CA8DrE"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAmBvD,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EACpB,KAAK,aAAa,EACnB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAA+B,iBAAiB,EAAE,mBAAmB,EAAgB,MAAM,iBAAiB,CAAA;AAEnH,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,CAAA;AACjD,YAAY,EAAE,aAAa,EAAE,CAAA;AAyF7B,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CAyxChF;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA0ChG"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAoBvD,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;AAEzI,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,CAw7ChF;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA0ChG"}
@@ -4,7 +4,7 @@
4
4
  * Manages in-memory LRU caches for session and fingerprint lookups,
5
5
  * coordinates with the shared file store for cross-proxy session resume.
6
6
  */
7
- import { type LineageResult } from "./lineage";
7
+ import { type SessionState, type TokenUsage, type LineageResult } from "./lineage";
8
8
  export declare function getMaxSessionsLimit(): number;
9
9
  /** Clear all session caches (used in tests).
10
10
  * Re-reads MERIDIAN_MAX_SESSIONS / CLAUDE_PROXY_MAX_SESSIONS so tests can override the limit. */
@@ -22,11 +22,16 @@ export declare function lookupSession(sessionId: string | undefined, messages: A
22
22
  role: string;
23
23
  content: any;
24
24
  }>, workingDirectory?: string): LineageResult;
25
+ /** Look up a session by the Claude SDK session ID returned in responses.
26
+ * Searches both in-memory caches and the shared file store, returning the
27
+ * freshest matching state if multiple cache keys point to the same Claude session. */
28
+ export declare function getSessionByClaudeId(claudeSessionId: string): SessionState | undefined;
25
29
  /** Store a session mapping with lineage hash and SDK UUIDs for divergence detection.
26
30
  * @param sdkMessageUuids — per-message SDK assistant UUIDs (null for user messages).
27
- * If provided, merged with any previously stored UUIDs to build a complete map. */
31
+ * If provided, merged with any previously stored UUIDs to build a complete map.
32
+ * @param contextUsage — optional last observed token usage to attach to the session. */
28
33
  export declare function storeSession(sessionId: string | undefined, messages: Array<{
29
34
  role: string;
30
- content: any;
31
- }>, claudeSessionId: string, workingDirectory?: string, sdkMessageUuids?: Array<string | null>): void;
35
+ content: unknown;
36
+ }>, claudeSessionId: string, workingDirectory?: string, sdkMessageUuids?: Array<string | null>, contextUsage?: TokenUsage): void;
32
37
  //# sourceMappingURL=cache.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../src/proxy/session/cache.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,WAAW,CAAA;AAMlB,wBAAgB,mBAAmB,IAAI,MAAM,CAW5C;AAqCD;kGACkG;AAClG,wBAAgB,iBAAiB,SAYhC;AAED;iFACiF;AACjF,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,gBAAgB,CAAC,EAAE,MAAM,EACzB,QAAQ,CAAC,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,GAC/C,IAAI,CAoBN;AAUD;;uDAEuD;AACvD,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,EAC/C,gBAAgB,CAAC,EAAE,MAAM,GACxB,aAAa,CAqDf;AAED;;sFAEsF;AACtF,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,EAC/C,eAAe,EAAE,MAAM,EACvB,gBAAgB,CAAC,EAAE,MAAM,EACzB,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,QAoBvC"}
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../src/proxy/session/cache.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,aAAa,EACnB,MAAM,WAAW,CAAA;AAMlB,wBAAgB,mBAAmB,IAAI,MAAM,CAW5C;AAqCD;kGACkG;AAClG,wBAAgB,iBAAiB,SAYhC;AAED;iFACiF;AACjF,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,gBAAgB,CAAC,EAAE,MAAM,EACzB,QAAQ,CAAC,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,GAC/C,IAAI,CAoBN;AAUD;;uDAEuD;AACvD,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,EAC/C,gBAAgB,CAAC,EAAE,MAAM,GACxB,aAAa,CAuDf;AAED;;uFAEuF;AACvF,wBAAgB,oBAAoB,CAAC,eAAe,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CA2BtF;AAED;;;yFAGyF;AACzF,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,EACnD,eAAe,EAAE,MAAM,EACvB,gBAAgB,CAAC,EAAE,MAAM,EACzB,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,EACtC,YAAY,CAAC,EAAE,UAAU,QA+B1B"}
@@ -4,6 +4,13 @@
4
4
  * Pure functions for hashing messages and classifying mutations
5
5
  * (continuation, compaction, undo, diverged).
6
6
  */
7
+ /** Token usage counters from the SDK (subset of Anthropic usage object). */
8
+ export interface TokenUsage {
9
+ input_tokens?: number;
10
+ output_tokens?: number;
11
+ cache_read_input_tokens?: number;
12
+ cache_creation_input_tokens?: number;
13
+ }
7
14
  /** Minimum suffix overlap (stored messages found at the end of incoming)
8
15
  * required to classify a mutation as compaction rather than a branch. */
9
16
  export declare const MIN_SUFFIX_FOR_COMPACTION = 2;
@@ -23,6 +30,8 @@ export interface SessionState {
23
30
  * Only assistant messages have UUIDs (user messages are null).
24
31
  * Used to find the rollback point for undo. */
25
32
  sdkMessageUuids?: Array<string | null>;
33
+ /** Last observed token usage for this session (from SDK message_start / message_delta events) */
34
+ contextUsage?: TokenUsage;
26
35
  }
27
36
  /**
28
37
  * Result of lineage verification — classifies the mutation and provides
@@ -1 +1 @@
1
- {"version":3,"file":"lineage.d.ts","sourceRoot":"","sources":["../../../src/proxy/session/lineage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH;0EAC0E;AAC1E,eAAO,MAAM,yBAAyB,IAAI,CAAA;AAE1C,MAAM,WAAW,YAAY;IAC3B,eAAe,EAAE,MAAM,CAAA;IACvB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB;;qDAEiD;IACjD,WAAW,EAAE,MAAM,CAAA;IACnB;;kCAE8B;IAC9B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;oDAEgD;IAChD,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CACvC;AAED;;;GAGG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,YAAY,CAAC;IAAG,OAAO,EAAE,YAAY,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAS,OAAO,EAAE,YAAY,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GACxG;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAA;AAIxB;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,GAAG,MAAM,CAI1F;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,GAAG,MAAM,CAK3E;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,GAAG,MAAM,EAAE,CAG9F;AAID;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,CAO7F;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,CAO7F;AAID,yEAAyE;AACzE,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAC7B;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,EAC/C,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,gBAAgB,GACtB,aAAa,CAqFf"}
1
+ {"version":3,"file":"lineage.d.ts","sourceRoot":"","sources":["../../../src/proxy/session/lineage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,4EAA4E;AAC5E,MAAM,WAAW,UAAU;IACzB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,2BAA2B,CAAC,EAAE,MAAM,CAAA;CACrC;AAED;0EAC0E;AAC1E,eAAO,MAAM,yBAAyB,IAAI,CAAA;AAE1C,MAAM,WAAW,YAAY;IAC3B,eAAe,EAAE,MAAM,CAAA;IACvB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB;;qDAEiD;IACjD,WAAW,EAAE,MAAM,CAAA;IACnB;;kCAE8B;IAC9B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;oDAEgD;IAChD,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACtC,iGAAiG;IACjG,YAAY,CAAC,EAAE,UAAU,CAAA;CAC1B;AAED;;;GAGG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,YAAY,CAAC;IAAG,OAAO,EAAE,YAAY,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAS,OAAO,EAAE,YAAY,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GACxG;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAA;AAIxB;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,GAAG,MAAM,CAI1F;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,GAAG,MAAM,CAK3E;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,GAAG,MAAM,EAAE,CAG9F;AAID;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,CAO7F;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,MAAM,CAO7F;AAID,yEAAyE;AACzE,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAC7B;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,EAC/C,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,gBAAgB,GACtB,aAAa,CAqFf"}
@@ -9,6 +9,7 @@
9
9
  * Format: { [key]: { claudeSessionId, createdAt, lastUsedAt } }
10
10
  * Keys are either OpenCode session IDs or conversation fingerprints.
11
11
  */
12
+ import type { TokenUsage } from "./session/lineage";
12
13
  export interface StoredSession {
13
14
  claudeSessionId: string;
14
15
  createdAt: number;
@@ -20,6 +21,8 @@ export interface StoredSession {
20
21
  messageHashes?: string[];
21
22
  /** Per-message SDK assistant UUIDs for undo rollback (null for user messages) */
22
23
  sdkMessageUuids?: Array<string | null>;
24
+ /** Last observed token usage for this Claude session */
25
+ contextUsage?: TokenUsage;
23
26
  }
24
27
  /** Set an explicit session store directory. Takes priority over env var.
25
28
  * Pass null to clear. For testing only.
@@ -28,7 +31,8 @@ export declare function setSessionStoreDir(dir: string | null, opts?: {
28
31
  skipLocking?: boolean;
29
32
  }): void;
30
33
  export declare function lookupSharedSession(key: string): StoredSession | undefined;
31
- export declare function storeSharedSession(key: string, claudeSessionId: string, messageCount?: number, lineageHash?: string, messageHashes?: string[], sdkMessageUuids?: Array<string | null>): void;
34
+ export declare function lookupSharedSessionByClaudeId(claudeSessionId: string): StoredSession | undefined;
35
+ export declare function storeSharedSession(key: string, claudeSessionId: string, messageCount?: number, lineageHash?: string, messageHashes?: string[], sdkMessageUuids?: Array<string | null>, contextUsage?: TokenUsage): void;
32
36
  /** Remove a single session from the shared file store.
33
37
  * Used when a session is detected as stale (e.g. expired upstream). */
34
38
  export declare function evictSharedSession(key: string): void;
@@ -1 +1 @@
1
- {"version":3,"file":"sessionStore.d.ts","sourceRoot":"","sources":["../../src/proxy/sessionStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAgBH,MAAM,WAAW,aAAa;IAC5B,eAAe,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,iFAAiF;IACjF,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CACvC;AA0DD;;oFAEoF;AACpF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAG7F;AAsED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAG1E;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,EAAE,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,IAAI,CAqC5L;AAED;wEACwE;AACxE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAkBpD;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAO1C"}
1
+ {"version":3,"file":"sessionStore.d.ts","sourceRoot":"","sources":["../../src/proxy/sessionStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAeH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAEnD,MAAM,WAAW,aAAa;IAC5B,eAAe,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,iFAAiF;IACjF,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACtC,wDAAwD;IACxD,YAAY,CAAC,EAAE,UAAU,CAAA;CAC1B;AA0DD;;oFAEoF;AACpF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAG7F;AAsED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAG1E;AAED,wBAAgB,6BAA6B,CAAC,eAAe,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAYhG;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,eAAe,EAAE,MAAM,EACvB,YAAY,CAAC,EAAE,MAAM,EACrB,WAAW,CAAC,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,MAAM,EAAE,EACxB,eAAe,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,EACtC,YAAY,CAAC,EAAE,UAAU,GACxB,IAAI,CAsCN;AAED;wEACwE;AACxE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAkBpD;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAO1C"}
package/dist/server.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  getMaxSessionsLimit,
7
7
  hashMessage,
8
8
  startProxyServer
9
- } from "./cli-s6f9jefk.js";
9
+ } from "./cli-yve9q0a0.js";
10
10
  import"./cli-rtab0qa6.js";
11
11
  import"./cli-m9pfb7h9.js";
12
12
  import"./cli-a05ws7rb.js";
package/package.json CHANGED
@@ -1,78 +1,78 @@
1
1
  {
2
- "name": "@rynfar/meridian",
3
- "version": "1.25.1",
4
- "description": "Local Anthropic API powered by your Claude Max subscription. One subscription, every agent.",
5
- "type": "module",
6
- "main": "./dist/server.js",
7
- "types": "./dist/proxy/server.d.ts",
8
- "bin": {
9
- "meridian": "./dist/cli.js",
10
- "claude-max-proxy": "./dist/cli.js"
11
- },
12
- "exports": {
13
- ".": {
14
- "types": "./dist/proxy/server.d.ts",
15
- "default": "./dist/server.js"
16
- }
17
- },
18
- "engines": {
19
- "node": ">=22"
20
- },
21
- "scripts": {
22
- "start": "./bin/claude-proxy-supervisor.sh",
23
- "proxy": "./bin/claude-proxy-supervisor.sh",
24
- "build": "rm -rf dist && bun build bin/cli.ts src/proxy/server.ts --outdir dist --target node --splitting --external @anthropic-ai/claude-agent-sdk --entry-naming '[name].js' && tsc -p tsconfig.build.json",
25
- "postbuild": "node --check dist/cli.js && node --check dist/server.js && test -f dist/proxy/server.d.ts",
26
- "prepublishOnly": "bun run build",
27
- "test": "bun test --path-ignore-patterns '**/*session-store*' --path-ignore-patterns '**/*proxy-async-ops*' --path-ignore-patterns '**/*proxy-extra-usage-fallback*' --path-ignore-patterns '**/*models-auth-status*' && bun test src/__tests__/proxy-extra-usage-fallback.test.ts && bun test src/__tests__/proxy-async-ops.test.ts && bun test src/__tests__/proxy-session-store.test.ts && bun test src/__tests__/session-store-pruning.test.ts && bun test src/__tests__/proxy-session-store-locking.test.ts && bun test src/__tests__/models-auth-status.test.ts",
28
- "typecheck": "tsc --noEmit",
29
- "proxy:direct": "bun run ./bin/cli.ts"
30
- },
31
- "dependencies": {
32
- "@anthropic-ai/claude-agent-sdk": "^0.2.80"
33
- },
34
- "devDependencies": {
35
- "@hono/node-server": "^1.19.11",
36
- "@types/bun": "^1.3.11",
37
- "@types/node": "^22.0.0",
38
- "glob": "^13.0.0",
39
- "hono": "^4.11.4",
40
- "typescript": "^5.8.2"
41
- },
42
- "files": [
43
- "dist/",
44
- "plugin/",
45
- "assets/",
46
- "README.md"
47
- ],
48
- "keywords": [
49
- "meridian",
50
- "claude",
51
- "claude-max",
52
- "claude-code",
53
- "anthropic",
54
- "anthropic-api",
55
- "claude-agent-sdk",
56
- "ai-coding",
57
- "ai-assistant",
58
- "proxy",
59
- "opencode",
60
- "cline",
61
- "aider",
62
- "llm",
63
- "coding-assistant",
64
- "opencode-claude-max-proxy"
65
- ],
66
- "repository": {
67
- "type": "git",
68
- "url": "git+https://github.com/rynfar/meridian.git"
69
- },
70
- "homepage": "https://github.com/rynfar/meridian#readme",
71
- "bugs": {
72
- "url": "https://github.com/rynfar/meridian/issues"
73
- },
74
- "author": "rynfar",
75
- "license": "MIT",
76
- "private": false,
77
- "packageManager": "bun@1.3.11"
2
+ "name": "@rynfar/meridian",
3
+ "version": "1.26.5",
4
+ "description": "Local Anthropic API powered by your Claude Max subscription. One subscription, every agent.",
5
+ "type": "module",
6
+ "main": "./dist/server.js",
7
+ "types": "./dist/proxy/server.d.ts",
8
+ "bin": {
9
+ "meridian": "./dist/cli.js",
10
+ "claude-max-proxy": "./dist/cli.js"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/proxy/server.d.ts",
15
+ "default": "./dist/server.js"
16
+ }
17
+ },
18
+ "engines": {
19
+ "node": ">=22"
20
+ },
21
+ "scripts": {
22
+ "start": "./bin/claude-proxy-supervisor.sh",
23
+ "proxy": "./bin/claude-proxy-supervisor.sh",
24
+ "build": "rm -rf dist && bun build bin/cli.ts src/proxy/server.ts --outdir dist --target node --splitting --external @anthropic-ai/claude-agent-sdk --entry-naming '[name].js' && tsc -p tsconfig.build.json",
25
+ "postbuild": "node --check dist/cli.js && node --check dist/server.js && test -f dist/proxy/server.d.ts",
26
+ "prepublishOnly": "bun run build",
27
+ "test": "bun test --path-ignore-patterns '**/*session-store*' --path-ignore-patterns '**/*proxy-async-ops*' --path-ignore-patterns '**/*proxy-extra-usage-fallback*' --path-ignore-patterns '**/*models-auth-status*' --path-ignore-patterns '**/*proxy-context-usage-store*' && bun test src/__tests__/proxy-extra-usage-fallback.test.ts && bun test src/__tests__/proxy-async-ops.test.ts && bun test src/__tests__/proxy-session-store.test.ts && bun test src/__tests__/session-store-pruning.test.ts && bun test src/__tests__/proxy-session-store-locking.test.ts && bun test src/__tests__/proxy-context-usage-store.test.ts && bun test src/__tests__/models-auth-status.test.ts",
28
+ "typecheck": "tsc --noEmit",
29
+ "proxy:direct": "bun run ./bin/cli.ts"
30
+ },
31
+ "dependencies": {
32
+ "@anthropic-ai/claude-agent-sdk": "^0.2.89"
33
+ },
34
+ "devDependencies": {
35
+ "@hono/node-server": "^1.19.11",
36
+ "@types/bun": "^1.3.11",
37
+ "@types/node": "^22.0.0",
38
+ "glob": "^13.0.0",
39
+ "hono": "^4.11.4",
40
+ "typescript": "^5.8.2"
41
+ },
42
+ "files": [
43
+ "dist/",
44
+ "plugin/",
45
+ "assets/",
46
+ "README.md"
47
+ ],
48
+ "keywords": [
49
+ "meridian",
50
+ "claude",
51
+ "claude-max",
52
+ "claude-code",
53
+ "anthropic",
54
+ "anthropic-api",
55
+ "claude-agent-sdk",
56
+ "ai-coding",
57
+ "ai-assistant",
58
+ "proxy",
59
+ "opencode",
60
+ "cline",
61
+ "aider",
62
+ "llm",
63
+ "coding-assistant",
64
+ "opencode-claude-max-proxy"
65
+ ],
66
+ "repository": {
67
+ "type": "git",
68
+ "url": "git+https://github.com/rynfar/meridian.git"
69
+ },
70
+ "homepage": "https://github.com/rynfar/meridian#readme",
71
+ "bugs": {
72
+ "url": "https://github.com/rynfar/meridian/issues"
73
+ },
74
+ "author": "rynfar",
75
+ "license": "MIT",
76
+ "private": false,
77
+ "packageManager": "bun@1.3.11"
78
78
  }