@rynfar/meridian 1.57.0 → 1.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/{cli-sz964j04.js → cli-5rkcyzzm.js} +141 -77
- package/dist/{cli-ngtexmne.js → cli-h6hfkg3s.js} +30 -2
- package/dist/{cli-aq5zz92m.js → cli-khhjyk04.js} +62 -3
- package/dist/{cli-bsg2dd52.js → cli-p3ggjwgn.js} +23 -2
- package/dist/{cli-340h1chz.js → cli-vj9cv18n.js} +11 -6
- package/dist/cli.js +8 -8
- package/dist/{profileCli-00z04stw.js → profileCli-c8bejc7w.js} +3 -3
- package/dist/{profiles-sq9t3fh9.js → profiles-3rg4x7cz.js} +2 -2
- package/dist/proxy/models.d.ts +2 -0
- package/dist/proxy/models.d.ts.map +1 -1
- package/dist/proxy/openaiResponses.d.ts +4 -2
- package/dist/proxy/openaiResponses.d.ts.map +1 -1
- package/dist/proxy/rateLimitStore.d.ts +26 -22
- package/dist/proxy/rateLimitStore.d.ts.map +1 -1
- package/dist/proxy/routing.d.ts +24 -1
- package/dist/proxy/routing.d.ts.map +1 -1
- package/dist/proxy/server.d.ts.map +1 -1
- package/dist/proxy/session/cache.d.ts.map +1 -1
- package/dist/proxy/session/fingerprint.d.ts +26 -0
- package/dist/proxy/session/fingerprint.d.ts.map +1 -1
- package/dist/proxy/session/lineage.d.ts +15 -9
- package/dist/proxy/session/lineage.d.ts.map +1 -1
- package/dist/proxy/settings.d.ts.map +1 -1
- package/dist/proxy/tokenRefresh.d.ts +41 -0
- package/dist/proxy/tokenRefresh.d.ts.map +1 -1
- package/dist/server.js +5 -5
- package/dist/{tokenRefresh-tkeg8wjq.js → tokenRefresh-kzz08kea.js} +10 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -141,9 +141,9 @@ This error class ([#516](https://github.com/rynfar/meridian/issues/516), histori
|
|
|
141
141
|
If you still hit the error on a current release, first check `GET /v1/usage/quota` to rule out genuinely exhausted quota, then try disabling the connecting client's system prompt for the affected adapter while keeping the Claude Code prompt enabled (in the `/settings` UI under **SDK Feature Toggles**, or `PATCH /settings/api/features/<adapter>` with `{"clientSystemPrompt":false,"codeSystemPrompt":true}`) — and please report it on [#516](https://github.com/rynfar/meridian/issues/516) with your plan type, since remaining occurrences are likely account-cohort specific (Team plans are treated differently by the API).
|
|
142
142
|
|
|
143
143
|
**I'm hitting rate limits on 1M context. What do I do?**
|
|
144
|
-
Meridian defaults Sonnet to 200k context because Sonnet 1M is always billed as Extra Usage on Max plans — even when regular usage isn't exhausted. This is [Anthropic's intended billing model](https://code.claude.com/docs/en/model-config#extended-context), not a bug. Set `MERIDIAN_SONNET_MODEL=sonnet[1m]` to opt in if you have Extra Usage enabled and understand the billing implications. Opus defaults to 1M context, which is included with Max/Team/Enterprise subscriptions at no extra cost. Note: there is a [known upstream bug](https://github.com/anthropics/claude-code/issues/39841) where Claude Code incorrectly gates Opus 1M behind Extra Usage on Max — this is Anthropic's to fix.
|
|
144
|
+
Meridian defaults Sonnet to 200k context because Sonnet 1M is always billed as Extra Usage on Max plans — even when regular usage isn't exhausted. This is [Anthropic's intended billing model](https://code.claude.com/docs/en/model-config#extended-context), not a bug. Set `MERIDIAN_SONNET_MODEL=sonnet[1m]` to opt in if you have Extra Usage enabled and understand the billing implications. Opus defaults to 1M context, which is included with Max/Team/Enterprise subscriptions at no extra cost. Fable defaults to 1M too — verified as included on Max and Team accounts at no Extra Usage cost — and Mythos, which rides the Fable tier, inherits the same default. Note: there is a [known upstream bug](https://github.com/anthropics/claude-code/issues/39841) where Claude Code incorrectly gates Opus 1M behind Extra Usage on Max — this is Anthropic's to fix.
|
|
145
145
|
|
|
146
|
-
To turn off 1M context entirely for **every** model (so Meridian never requests the extended window), set `MERIDIAN_1M_CONTEXT_SUPPORT=0`. Meridian also auto-detects the "out of extra usage" error, falls back to the 200k model, and skips 1M for an hour — so it self-heals after the first occurrence even without the env var.
|
|
146
|
+
To turn off 1M context entirely for **every** model (so Meridian never requests the extended window), set `MERIDIAN_1M_CONTEXT_SUPPORT=0`. To back off a single tier instead — without giving up the other tier's included 1M context — set `MERIDIAN_FABLE_MODEL=fable` or `MERIDIAN_OPUS_MODEL=opus` (both also accept the `CLAUDE_PROXY_` prefix). Meridian also auto-detects the "out of extra usage" error, falls back to the 200k model, and skips 1M for an hour — so it self-heals after the first occurrence even without the env var.
|
|
147
147
|
|
|
148
148
|
**Why does the health endpoint show `"plugin": "not-configured"`?**
|
|
149
149
|
You haven't run `meridian setup`. Without the plugin, OpenCode requests won't have session tracking or subagent model selection. Run `meridian setup` and restart OpenCode.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AssignmentStore,
|
|
2
3
|
ProfileExhaustion,
|
|
3
4
|
choosePriorityProfile,
|
|
4
5
|
getActiveProfileId,
|
|
@@ -9,7 +10,7 @@ import {
|
|
|
9
10
|
resolveProfile,
|
|
10
11
|
restoreActiveProfile,
|
|
11
12
|
setActiveProfile
|
|
12
|
-
} from "./cli-
|
|
13
|
+
} from "./cli-h6hfkg3s.js";
|
|
13
14
|
import {
|
|
14
15
|
isTrackedPlugin,
|
|
15
16
|
recordError,
|
|
@@ -47,11 +48,11 @@ import {
|
|
|
47
48
|
resolvePassthrough,
|
|
48
49
|
resolveSdkModelDefaults,
|
|
49
50
|
stripExtendedContext
|
|
50
|
-
} from "./cli-
|
|
51
|
+
} from "./cli-p3ggjwgn.js";
|
|
51
52
|
import {
|
|
52
53
|
getSetting,
|
|
53
54
|
setSetting
|
|
54
|
-
} from "./cli-
|
|
55
|
+
} from "./cli-vj9cv18n.js";
|
|
55
56
|
import {
|
|
56
57
|
checkPluginConfigured
|
|
57
58
|
} from "./cli-je60fevk.js";
|
|
@@ -59,11 +60,13 @@ import {
|
|
|
59
60
|
claudeLog,
|
|
60
61
|
createPlatformCredentialStore,
|
|
61
62
|
ensureFreshToken,
|
|
63
|
+
getAuthRenewalStatus,
|
|
62
64
|
refreshOAuthToken,
|
|
65
|
+
resolveRenewalWarnDays,
|
|
63
66
|
startBackgroundRefresh,
|
|
64
67
|
stopBackgroundRefresh,
|
|
65
68
|
withClaudeLogContext
|
|
66
|
-
} from "./cli-
|
|
69
|
+
} from "./cli-khhjyk04.js";
|
|
67
70
|
import {
|
|
68
71
|
__commonJS,
|
|
69
72
|
__esm,
|
|
@@ -4364,24 +4367,40 @@ import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
|
4364
4367
|
|
|
4365
4368
|
// src/proxy/rateLimitStore.ts
|
|
4366
4369
|
class RateLimitStore {
|
|
4367
|
-
|
|
4368
|
-
record(info, observedAt = Date.now()) {
|
|
4370
|
+
byProfile = new Map;
|
|
4371
|
+
record(profileId, info, observedAt = Date.now()) {
|
|
4369
4372
|
if (!info || typeof info !== "object")
|
|
4370
4373
|
return;
|
|
4371
4374
|
const key = info.rateLimitType ?? "default";
|
|
4372
|
-
this.
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
return this.entries.get(key);
|
|
4379
|
-
}
|
|
4380
|
-
get size() {
|
|
4381
|
-
return this.entries.size;
|
|
4375
|
+
let buckets = this.byProfile.get(profileId);
|
|
4376
|
+
if (!buckets) {
|
|
4377
|
+
buckets = new Map;
|
|
4378
|
+
this.byProfile.set(profileId, buckets);
|
|
4379
|
+
}
|
|
4380
|
+
buckets.set(key, { ...info, observedAt });
|
|
4382
4381
|
}
|
|
4383
|
-
|
|
4384
|
-
this.
|
|
4382
|
+
getAll(profileId) {
|
|
4383
|
+
const buckets = this.byProfile.get(profileId);
|
|
4384
|
+
if (!buckets)
|
|
4385
|
+
return [];
|
|
4386
|
+
return Array.from(buckets.values()).sort((a, b) => b.observedAt - a.observedAt);
|
|
4387
|
+
}
|
|
4388
|
+
get(profileId, key) {
|
|
4389
|
+
return this.byProfile.get(profileId)?.get(key);
|
|
4390
|
+
}
|
|
4391
|
+
size(profileId) {
|
|
4392
|
+
if (profileId !== undefined)
|
|
4393
|
+
return this.byProfile.get(profileId)?.size ?? 0;
|
|
4394
|
+
let total = 0;
|
|
4395
|
+
for (const buckets of this.byProfile.values())
|
|
4396
|
+
total += buckets.size;
|
|
4397
|
+
return total;
|
|
4398
|
+
}
|
|
4399
|
+
clear(profileId) {
|
|
4400
|
+
if (profileId !== undefined)
|
|
4401
|
+
this.byProfile.delete(profileId);
|
|
4402
|
+
else
|
|
4403
|
+
this.byProfile.clear();
|
|
4385
4404
|
}
|
|
4386
4405
|
}
|
|
4387
4406
|
var rateLimitStore = new RateLimitStore;
|
|
@@ -11086,13 +11105,25 @@ function buildModelList(isMaxSubscription, now = Math.floor(Date.now() / 1000))
|
|
|
11086
11105
|
}
|
|
11087
11106
|
|
|
11088
11107
|
// src/proxy/openaiResponses.ts
|
|
11108
|
+
function itemDiscriminator(item) {
|
|
11109
|
+
if (typeof item !== "object" || item === null)
|
|
11110
|
+
return;
|
|
11111
|
+
const record = item;
|
|
11112
|
+
if (typeof record.type === "string")
|
|
11113
|
+
return record.type;
|
|
11114
|
+
if (record.type === undefined && "role" in record)
|
|
11115
|
+
return "message";
|
|
11116
|
+
return;
|
|
11117
|
+
}
|
|
11089
11118
|
function partsToText(content) {
|
|
11119
|
+
if (content === undefined)
|
|
11120
|
+
return "";
|
|
11090
11121
|
if (typeof content === "string")
|
|
11091
11122
|
return content;
|
|
11092
11123
|
return content.filter((p) => typeof p.text === "string").map((p) => p.text).join("");
|
|
11093
11124
|
}
|
|
11094
11125
|
function partsToBlocks(content) {
|
|
11095
|
-
if (typeof content === "string")
|
|
11126
|
+
if (content === undefined || typeof content === "string")
|
|
11096
11127
|
return null;
|
|
11097
11128
|
const hasImage = content.some((p) => p.type === "input_image");
|
|
11098
11129
|
if (!hasImage)
|
|
@@ -11141,7 +11172,7 @@ function translateResponsesToAnthropic(body) {
|
|
|
11141
11172
|
}
|
|
11142
11173
|
};
|
|
11143
11174
|
for (const item of items) {
|
|
11144
|
-
switch (item
|
|
11175
|
+
switch (itemDiscriminator(item)) {
|
|
11145
11176
|
case "message": {
|
|
11146
11177
|
const msg = item;
|
|
11147
11178
|
if (msg.role === "developer" || msg.role === "system") {
|
|
@@ -11833,6 +11864,12 @@ function getConversationFingerprint(messages, workingDirectory) {
|
|
|
11833
11864
|
${text.slice(0, 2000)}` : text.slice(0, 2000);
|
|
11834
11865
|
return createHash("sha256").update(seed).digest("hex").slice(0, 16);
|
|
11835
11866
|
}
|
|
11867
|
+
function getPriorityAssignmentKey(sessionId, messages, workingDirectory) {
|
|
11868
|
+
if (sessionId)
|
|
11869
|
+
return sessionId;
|
|
11870
|
+
const fingerprint = getConversationFingerprint(messages, workingDirectory);
|
|
11871
|
+
return fingerprint ? `fp:${fingerprint}` : null;
|
|
11872
|
+
}
|
|
11836
11873
|
|
|
11837
11874
|
// src/proxy/adapters/opencode.ts
|
|
11838
11875
|
init_env();
|
|
@@ -19189,36 +19226,34 @@ function findSuffixAnchorStart(storedHashes, incomingHashes, suffixOverlap) {
|
|
|
19189
19226
|
return -1;
|
|
19190
19227
|
return anchor - suffixOverlap + 1;
|
|
19191
19228
|
}
|
|
19192
|
-
function verifyLineage(cached, messages
|
|
19229
|
+
function verifyLineage(cached, messages) {
|
|
19193
19230
|
if (!cached.lineageHash || cached.messageCount === 0) {
|
|
19194
|
-
return { type: "
|
|
19231
|
+
return { type: "diverged", reason: "unverifiable" };
|
|
19195
19232
|
}
|
|
19196
19233
|
const prefix = messages.slice(0, cached.messageCount);
|
|
19197
19234
|
const prefixHash = computeLineageHash(prefix);
|
|
19198
19235
|
if (prefixHash === cached.lineageHash) {
|
|
19199
19236
|
if (messages.length <= cached.messageCount) {
|
|
19200
|
-
|
|
19201
|
-
return { type: "diverged" };
|
|
19237
|
+
return { type: "diverged", reason: "replayed-request" };
|
|
19202
19238
|
}
|
|
19203
|
-
return { type: "continuation", session: cached };
|
|
19239
|
+
return { type: "continuation", session: cached, resumeFrom: cached.messageCount };
|
|
19204
19240
|
}
|
|
19205
19241
|
if (!cached.messageHashes || cached.messageHashes.length === 0) {
|
|
19206
|
-
|
|
19207
|
-
return { type: "diverged" };
|
|
19242
|
+
return { type: "diverged", reason: "unverifiable" };
|
|
19208
19243
|
}
|
|
19209
19244
|
const incomingHashes = computeMessageHashes(messages);
|
|
19210
19245
|
const prefixOverlap = measurePrefixOverlap(cached.messageHashes, incomingHashes);
|
|
19211
19246
|
const suffixOverlap = measureSuffixOverlap(cached.messageHashes, incomingHashes);
|
|
19212
19247
|
const MIN_STORED_FOR_COMPACTION = 6;
|
|
19213
19248
|
const suffixStartInIncoming = incomingHashes.length - suffixOverlap >= 0 ? findSuffixAnchorStart(cached.messageHashes, incomingHashes, suffixOverlap) : -1;
|
|
19214
|
-
|
|
19215
|
-
|
|
19216
|
-
|
|
19217
|
-
|
|
19218
|
-
|
|
19219
|
-
|
|
19220
|
-
|
|
19221
|
-
|
|
19249
|
+
const compactionResumeFrom = suffixStartInIncoming + suffixOverlap;
|
|
19250
|
+
if (suffixOverlap >= MIN_SUFFIX_FOR_COMPACTION && cached.messageHashes.length >= MIN_STORED_FOR_COMPACTION && suffixStartInIncoming > 0 && compactionResumeFrom < messages.length) {
|
|
19251
|
+
return {
|
|
19252
|
+
type: "compaction",
|
|
19253
|
+
session: cached,
|
|
19254
|
+
resumeFrom: compactionResumeFrom,
|
|
19255
|
+
suffixOverlap
|
|
19256
|
+
};
|
|
19222
19257
|
}
|
|
19223
19258
|
if (prefixOverlap > 0 && suffixOverlap === 0 && messages.length <= cached.messageCount) {
|
|
19224
19259
|
let rollbackUuid;
|
|
@@ -19230,22 +19265,12 @@ function verifyLineage(cached, messages, cacheKey2, cache) {
|
|
|
19230
19265
|
}
|
|
19231
19266
|
}
|
|
19232
19267
|
}
|
|
19233
|
-
const undoMsg = `Undo detected (key=${cacheKey2.slice(0, 8)}…): prefix overlap ${prefixOverlap}/${cached.messageHashes.length}, rollback UUID: ${rollbackUuid || "none (legacy session)"}.`;
|
|
19234
|
-
console.error(`[PROXY] ${undoMsg}`);
|
|
19235
|
-
diagnosticLog2.lineage(undoMsg);
|
|
19236
19268
|
return { type: "undo", session: cached, prefixOverlap, rollbackUuid };
|
|
19237
19269
|
}
|
|
19238
19270
|
if (prefixOverlap > 0 && messages.length > cached.messageCount) {
|
|
19239
|
-
|
|
19240
|
-
console.error(`[PROXY] ${modifiedMsg}`);
|
|
19241
|
-
diagnosticLog2.lineage(modifiedMsg);
|
|
19242
|
-
cached.lineageHash = computeLineageHash(messages.slice(0, messages.length));
|
|
19243
|
-
cached.messageHashes = incomingHashes;
|
|
19244
|
-
cached.messageCount = messages.length;
|
|
19245
|
-
return { type: "continuation", session: cached };
|
|
19271
|
+
return { type: "diverged", reason: "modified-history", prefixOverlap };
|
|
19246
19272
|
}
|
|
19247
|
-
|
|
19248
|
-
return { type: "diverged" };
|
|
19273
|
+
return { type: "diverged", reason: "unrelated-history", prefixOverlap };
|
|
19249
19274
|
}
|
|
19250
19275
|
|
|
19251
19276
|
// src/proxy/sessionStore.ts
|
|
@@ -19546,11 +19571,28 @@ function touchSession(state) {
|
|
|
19546
19571
|
state.lastAccess = Date.now();
|
|
19547
19572
|
return state;
|
|
19548
19573
|
}
|
|
19574
|
+
function classifyLineage(state, messages, cacheKey2) {
|
|
19575
|
+
const result = verifyLineage(state, messages);
|
|
19576
|
+
if (result.type === "compaction") {
|
|
19577
|
+
const msg = `Compaction detected (key=${cacheKey2.slice(0, 8)}…): suffix overlap ${result.suffixOverlap}/${state.messageCount}, resume from incoming message ${result.resumeFrom}.`;
|
|
19578
|
+
console.error(`[PROXY] ${msg}`);
|
|
19579
|
+
diagnosticLog2.lineage(msg);
|
|
19580
|
+
} else if (result.type === "undo") {
|
|
19581
|
+
const msg = `Undo detected (key=${cacheKey2.slice(0, 8)}…): prefix overlap ${result.prefixOverlap}/${state.messageCount}, rollback UUID: ${result.rollbackUuid || "none (legacy session)"}.`;
|
|
19582
|
+
console.error(`[PROXY] ${msg}`);
|
|
19583
|
+
diagnosticLog2.lineage(msg);
|
|
19584
|
+
} else if (result.type === "diverged" && result.reason === "modified-history") {
|
|
19585
|
+
const msg = `Stale session detected (key=${cacheKey2.slice(0, 8)}…): prefix overlap ${result.prefixOverlap || 0}/${state.messageCount}, incoming ${messages.length} msgs. Starting fresh replay.`;
|
|
19586
|
+
console.error(`[PROXY] ${msg}`);
|
|
19587
|
+
diagnosticLog2.lineage(msg);
|
|
19588
|
+
}
|
|
19589
|
+
return result;
|
|
19590
|
+
}
|
|
19549
19591
|
function lookupSession(sessionId, messages, workingDirectory) {
|
|
19550
19592
|
if (sessionId) {
|
|
19551
19593
|
const cached = sessionCache.get(sessionId);
|
|
19552
19594
|
if (cached) {
|
|
19553
|
-
const result =
|
|
19595
|
+
const result = classifyLineage(cached, messages, sessionId);
|
|
19554
19596
|
if (result.type === "continuation" || result.type === "compaction")
|
|
19555
19597
|
touchSession(result.session);
|
|
19556
19598
|
return result;
|
|
@@ -19566,19 +19608,19 @@ function lookupSession(sessionId, messages, workingDirectory) {
|
|
|
19566
19608
|
sdkMessageUuids: shared.sdkMessageUuids,
|
|
19567
19609
|
contextUsage: shared.contextUsage
|
|
19568
19610
|
};
|
|
19569
|
-
const result =
|
|
19611
|
+
const result = classifyLineage(state, messages, sessionId);
|
|
19570
19612
|
if (result.type === "continuation" || result.type === "compaction") {
|
|
19571
19613
|
sessionCache.set(sessionId, state);
|
|
19572
19614
|
}
|
|
19573
19615
|
return result;
|
|
19574
19616
|
}
|
|
19575
|
-
return { type: "diverged" };
|
|
19617
|
+
return { type: "diverged", reason: "not-found" };
|
|
19576
19618
|
}
|
|
19577
19619
|
const fp = getConversationFingerprint(messages, workingDirectory);
|
|
19578
19620
|
if (fp) {
|
|
19579
19621
|
const cached = fingerprintCache.get(fp);
|
|
19580
19622
|
if (cached) {
|
|
19581
|
-
const result =
|
|
19623
|
+
const result = classifyLineage(cached, messages, fp);
|
|
19582
19624
|
if (result.type === "continuation" || result.type === "compaction")
|
|
19583
19625
|
touchSession(result.session);
|
|
19584
19626
|
return result;
|
|
@@ -19594,14 +19636,14 @@ function lookupSession(sessionId, messages, workingDirectory) {
|
|
|
19594
19636
|
sdkMessageUuids: shared.sdkMessageUuids,
|
|
19595
19637
|
contextUsage: shared.contextUsage
|
|
19596
19638
|
};
|
|
19597
|
-
const result =
|
|
19639
|
+
const result = classifyLineage(state, messages, fp);
|
|
19598
19640
|
if (result.type === "continuation" || result.type === "compaction") {
|
|
19599
19641
|
fingerprintCache.set(fp, state);
|
|
19600
19642
|
}
|
|
19601
19643
|
return result;
|
|
19602
19644
|
}
|
|
19603
19645
|
}
|
|
19604
|
-
return { type: "diverged" };
|
|
19646
|
+
return { type: "diverged", reason: "not-found" };
|
|
19605
19647
|
}
|
|
19606
19648
|
function getSessionByClaudeId(claudeSessionId) {
|
|
19607
19649
|
let newest;
|
|
@@ -19905,8 +19947,8 @@ function createProxyServer(config = {}) {
|
|
|
19905
19947
|
app.use("/settings", requireAuth);
|
|
19906
19948
|
app.use("/design-login", requireAuth);
|
|
19907
19949
|
const priorityExhaustion = new ProfileExhaustion;
|
|
19908
|
-
const priorityAssignments = new Map;
|
|
19909
19950
|
const PRIORITY_ASSIGNMENTS_MAX = 5000;
|
|
19951
|
+
const priorityAssignments = new AssignmentStore(PRIORITY_ASSIGNMENTS_MAX);
|
|
19910
19952
|
const PRIORITY_DEFAULT_COOLDOWN_MS = 10 * 60000;
|
|
19911
19953
|
const PRIORITY_COOLDOWN_CAP_MS = 6 * 60 * 60000;
|
|
19912
19954
|
function priorityProfileOrderSetting() {
|
|
@@ -19916,11 +19958,33 @@ function createProxyServer(config = {}) {
|
|
|
19916
19958
|
const setting = getSetting("profileOrder");
|
|
19917
19959
|
return Array.isArray(setting) && setting.length > 0 ? setting : undefined;
|
|
19918
19960
|
}
|
|
19919
|
-
function priorityCooldownUntil(now) {
|
|
19920
|
-
const fiveHour = rateLimitStore.getAll().find((e) => e.rateLimitType === "five_hour" && (e.resetsAt ?? 0) > now);
|
|
19961
|
+
function priorityCooldownUntil(profileId, now) {
|
|
19962
|
+
const fiveHour = rateLimitStore.getAll(profileId).find((e) => e.rateLimitType === "five_hour" && (e.resetsAt ?? 0) > now && (e.status === "rejected" || (e.utilization ?? 0) >= 1));
|
|
19921
19963
|
const until = fiveHour?.resetsAt ?? now + PRIORITY_DEFAULT_COOLDOWN_MS;
|
|
19922
19964
|
return Math.min(until, now + PRIORITY_COOLDOWN_CAP_MS);
|
|
19923
19965
|
}
|
|
19966
|
+
function refinePriorityCooldown(profileId) {
|
|
19967
|
+
const target = getEffectiveProfiles(finalConfig.profiles).find((p) => p.id === profileId);
|
|
19968
|
+
fetchOAuthUsage({ profileId, claudeConfigDir: target?.claudeConfigDir, force: true }).then((usage) => {
|
|
19969
|
+
if (!usage || usage.stale)
|
|
19970
|
+
return;
|
|
19971
|
+
const fiveHour = usage.windows.find((w) => w.type === "five_hour");
|
|
19972
|
+
if (!fiveHour || (fiveHour.utilization ?? 0) < 1)
|
|
19973
|
+
return;
|
|
19974
|
+
const now = Date.now();
|
|
19975
|
+
const resetsAt = fiveHour.resetsAt;
|
|
19976
|
+
if (!resetsAt || resetsAt <= now)
|
|
19977
|
+
return;
|
|
19978
|
+
const until = Math.min(resetsAt, now + PRIORITY_COOLDOWN_CAP_MS);
|
|
19979
|
+
priorityExhaustion.mark(profileId, until, "rate_limit_error");
|
|
19980
|
+
claudeLog("priority.cooldown_refined", { profile: profileId, until, source: "oauth_usage" });
|
|
19981
|
+
}).catch((err) => {
|
|
19982
|
+
claudeLog("priority.cooldown_refine_failed", {
|
|
19983
|
+
profile: profileId,
|
|
19984
|
+
error: err instanceof Error ? err.message : String(err)
|
|
19985
|
+
});
|
|
19986
|
+
});
|
|
19987
|
+
}
|
|
19924
19988
|
async function sniffQuotaFailure(res) {
|
|
19925
19989
|
const contentType = res.headers.get("content-type") ?? "";
|
|
19926
19990
|
if (!contentType.includes("text/event-stream")) {
|
|
@@ -19995,22 +20059,18 @@ function createProxyServer(config = {}) {
|
|
|
19995
20059
|
const inner = await app.fetch(new Request(c.req.url, { method: "POST", headers, body: bodyBuf }));
|
|
19996
20060
|
const { failed, errorPayload, response } = await sniffQuotaFailure(inner);
|
|
19997
20061
|
if (!failed) {
|
|
19998
|
-
if (sessionKey)
|
|
20062
|
+
if (sessionKey)
|
|
19999
20063
|
priorityAssignments.set(sessionKey, candidate);
|
|
20000
|
-
if (priorityAssignments.size > PRIORITY_ASSIGNMENTS_MAX) {
|
|
20001
|
-
const oldest = priorityAssignments.keys().next().value;
|
|
20002
|
-
if (oldest !== undefined)
|
|
20003
|
-
priorityAssignments.delete(oldest);
|
|
20004
|
-
}
|
|
20005
|
-
}
|
|
20006
20064
|
if (previous) {
|
|
20007
20065
|
claudeLog("profile.failover", { from: previous, to: candidate, reason: "rate_limit_error", sessionKey });
|
|
20008
20066
|
plog(`[PROXY] PRIORITY failover ${previous} -> ${candidate}`);
|
|
20009
20067
|
}
|
|
20010
20068
|
return response;
|
|
20011
20069
|
}
|
|
20012
|
-
|
|
20013
|
-
|
|
20070
|
+
const cooldownUntil = priorityCooldownUntil(candidate, Date.now());
|
|
20071
|
+
priorityExhaustion.mark(candidate, cooldownUntil, "rate_limit_error");
|
|
20072
|
+
claudeLog("priority.exhausted", { profile: candidate, until: cooldownUntil });
|
|
20073
|
+
refinePriorityCooldown(candidate);
|
|
20014
20074
|
lastError = errorPayload;
|
|
20015
20075
|
previous = candidate;
|
|
20016
20076
|
}
|
|
@@ -20098,7 +20158,8 @@ data: ${JSON.stringify(lastError)}
|
|
|
20098
20158
|
const { order, unknown } = resolvePriorityOrder(effectivePool.map((p) => p.id), priorityProfileOrderSetting());
|
|
20099
20159
|
if (unknown.length > 0)
|
|
20100
20160
|
claudeLog("priority.unknown_order_ids", { unknown });
|
|
20101
|
-
const
|
|
20161
|
+
const assignmentCwd = adapter.extractClientWorkingDirectory?.(body) ?? adapter.extractWorkingDirectory(body);
|
|
20162
|
+
const sessionKey = getPriorityAssignmentKey(adapter.getSessionId(c, body), body.messages, assignmentCwd);
|
|
20102
20163
|
const assigned = sessionKey ? priorityAssignments.get(sessionKey) : undefined;
|
|
20103
20164
|
let first;
|
|
20104
20165
|
if (assigned && order.includes(assigned) && !priorityExhaustion.isExhausted(assigned)) {
|
|
@@ -20224,14 +20285,15 @@ data: ${JSON.stringify(lastError)}
|
|
|
20224
20285
|
claudeLog("session.pending_store_awaited", { waitedMs: Date.now() - waitStart });
|
|
20225
20286
|
}
|
|
20226
20287
|
}
|
|
20227
|
-
let lineageResult = isIndependentSession ? { type: "diverged" } : lookupSession(profileSessionId, body.messages || [], profileScopedCwd);
|
|
20288
|
+
let lineageResult = isIndependentSession ? { type: "diverged", reason: "independent-request" } : lookupSession(profileSessionId, body.messages || [], profileScopedCwd);
|
|
20228
20289
|
if (lineageResult.type === "undo" && adapterBase === "opencode" && !agentSessionId) {
|
|
20229
|
-
lineageResult = { type: "diverged" };
|
|
20290
|
+
lineageResult = { type: "diverged", reason: "missing-session-header" };
|
|
20230
20291
|
}
|
|
20231
20292
|
const isResume = lineageResult.type === "continuation" || lineageResult.type === "compaction";
|
|
20232
20293
|
const isUndo = lineageResult.type === "undo";
|
|
20233
20294
|
const cachedSession = lineageResult.type !== "diverged" ? lineageResult.session : undefined;
|
|
20234
20295
|
const resumeSessionId = cachedSession?.claudeSessionId;
|
|
20296
|
+
const resumeFrom = lineageResult.type === "continuation" || lineageResult.type === "compaction" ? lineageResult.resumeFrom : undefined;
|
|
20235
20297
|
const undoRollbackUuid = isUndo && lineageResult.type === "undo" ? lineageResult.rollbackUuid : undefined;
|
|
20236
20298
|
const msgSummary = body.messages?.map((m) => {
|
|
20237
20299
|
const contentTypes = Array.isArray(m.content) ? m.content.map((b) => b.type).join(",") : "string";
|
|
@@ -20274,9 +20336,8 @@ data: ${JSON.stringify(lastError)}
|
|
|
20274
20336
|
if (isUndo && undoRollbackUuid) {
|
|
20275
20337
|
messagesToConvert = getLastUserMessage(allMessages);
|
|
20276
20338
|
} else if (isResume) {
|
|
20277
|
-
|
|
20278
|
-
|
|
20279
|
-
messagesToConvert = allMessages.slice(knownCount);
|
|
20339
|
+
if (resumeFrom !== undefined && resumeFrom < allMessages.length) {
|
|
20340
|
+
messagesToConvert = allMessages.slice(resumeFrom);
|
|
20280
20341
|
} else {
|
|
20281
20342
|
messagesToConvert = getLastUserMessage(allMessages);
|
|
20282
20343
|
}
|
|
@@ -20569,7 +20630,7 @@ data: ${JSON.stringify(lastError)}
|
|
|
20569
20630
|
advisorModel
|
|
20570
20631
|
}, requestAbort.controller))) {
|
|
20571
20632
|
if (event.type === "rate_limit_event") {
|
|
20572
|
-
rateLimitStore.record(event.rate_limit_info);
|
|
20633
|
+
rateLimitStore.record(profile.id, event.rate_limit_info);
|
|
20573
20634
|
}
|
|
20574
20635
|
if (event.type === "assistant" && !event.error) {
|
|
20575
20636
|
didYieldContent = true;
|
|
@@ -21151,7 +21212,7 @@ data: ${JSON.stringify({ type: "content_block_stop", index: idx })}
|
|
|
21151
21212
|
advisorModel
|
|
21152
21213
|
}, requestAbort.controller))) {
|
|
21153
21214
|
if (event.type === "rate_limit_event") {
|
|
21154
|
-
rateLimitStore.record(event.rate_limit_info);
|
|
21215
|
+
rateLimitStore.record(profile.id, event.rate_limit_info);
|
|
21155
21216
|
}
|
|
21156
21217
|
if (event.type === "stream_event") {
|
|
21157
21218
|
didYieldClientEvent = true;
|
|
@@ -22229,13 +22290,17 @@ data: ${JSON.stringify({
|
|
|
22229
22290
|
}, 503);
|
|
22230
22291
|
}
|
|
22231
22292
|
const claudeExecutableInfo = getResolvedClaudeExecutableInfo();
|
|
22293
|
+
const warnDays = resolveRenewalWarnDays(process.env.MERIDIAN_AUTH_RENEWAL_WARN_DAYS);
|
|
22294
|
+
const renewalConfigDir = profileEnvOverrides?.CLAUDE_CONFIG_DIR;
|
|
22295
|
+
const renewal = await getAuthRenewalStatus(renewalConfigDir ? createPlatformCredentialStore({ claudeConfigDir: renewalConfigDir }) : undefined, warnDays).catch(() => ({ renewalRequiredSoon: false }));
|
|
22232
22296
|
return c.json({
|
|
22233
22297
|
status: "healthy",
|
|
22234
22298
|
version: serverVersion,
|
|
22235
22299
|
auth: {
|
|
22236
22300
|
loggedIn: true,
|
|
22237
22301
|
email: auth.email,
|
|
22238
|
-
subscriptionType: auth.subscriptionType
|
|
22302
|
+
subscriptionType: auth.subscriptionType,
|
|
22303
|
+
...renewal
|
|
22239
22304
|
},
|
|
22240
22305
|
mode: envBool("PASSTHROUGH") ? "passthrough" : "internal",
|
|
22241
22306
|
...claudeExecutableInfo ? { claudeExecutable: claudeExecutableInfo } : {},
|
|
@@ -22302,7 +22367,6 @@ data: ${JSON.stringify({
|
|
|
22302
22367
|
const previousProfile = getActiveProfileId() ?? null;
|
|
22303
22368
|
setActiveProfile(body.profile);
|
|
22304
22369
|
clearSessionCache();
|
|
22305
|
-
rateLimitStore.clear();
|
|
22306
22370
|
claudeLog("profile.switched", {
|
|
22307
22371
|
from: previousProfile,
|
|
22308
22372
|
to: body.profile,
|
|
@@ -22356,7 +22420,7 @@ data: ${JSON.stringify({
|
|
|
22356
22420
|
const store = credentialStoreForProfile(profile);
|
|
22357
22421
|
const success = store ? await refreshOAuthToken(store) : false;
|
|
22358
22422
|
if (success) {
|
|
22359
|
-
rateLimitStore.clear();
|
|
22423
|
+
rateLimitStore.clear(profile.id);
|
|
22360
22424
|
return c.json({ success: true, message: "OAuth token refreshed successfully", profile: profile.id });
|
|
22361
22425
|
}
|
|
22362
22426
|
return c.json({ success: false, message: "Token refresh failed. If the problem persists, run 'claude login'." }, 500);
|
|
@@ -22579,11 +22643,11 @@ data: ${JSON.stringify({ response: { id: responseId, status: "failed", error: {
|
|
|
22579
22643
|
return c.json({ object: "list", data: buildModelList(isMax) });
|
|
22580
22644
|
});
|
|
22581
22645
|
app.get("/v1/usage/quota", async (c) => {
|
|
22582
|
-
const sdkEntries = rateLimitStore.getAll().filter((entry) => entry.rateLimitType !== undefined);
|
|
22583
22646
|
const requestedProfile = c.req.query("profile");
|
|
22584
22647
|
const profilesList = getEffectiveProfiles(finalConfig.profiles);
|
|
22585
22648
|
const targetProfileId = requestedProfile || getActiveProfileId() || finalConfig.defaultProfile || profilesList[0]?.id || null;
|
|
22586
22649
|
const targetProfile = targetProfileId ? profilesList.find((p) => p.id === targetProfileId) : undefined;
|
|
22650
|
+
const sdkEntries = rateLimitStore.getAll(targetProfileId ?? "default").filter((entry) => entry.rateLimitType !== undefined);
|
|
22587
22651
|
const oauth = await fetchOAuthUsage({
|
|
22588
22652
|
profileId: targetProfileId ?? undefined,
|
|
22589
22653
|
claudeConfigDir: targetProfile?.claudeConfigDir
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getSetting,
|
|
3
3
|
setSetting
|
|
4
|
-
} from "./cli-
|
|
4
|
+
} from "./cli-vj9cv18n.js";
|
|
5
5
|
|
|
6
6
|
// src/proxy/profiles.ts
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -98,6 +98,34 @@ class ProfileExhaustion {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
class AssignmentStore {
|
|
102
|
+
max;
|
|
103
|
+
entries = new Map;
|
|
104
|
+
constructor(max) {
|
|
105
|
+
this.max = max;
|
|
106
|
+
}
|
|
107
|
+
get(key) {
|
|
108
|
+
const value = this.entries.get(key);
|
|
109
|
+
if (value === undefined)
|
|
110
|
+
return;
|
|
111
|
+
this.entries.delete(key);
|
|
112
|
+
this.entries.set(key, value);
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
set(key, value) {
|
|
116
|
+
this.entries.delete(key);
|
|
117
|
+
this.entries.set(key, value);
|
|
118
|
+
if (this.entries.size > this.max) {
|
|
119
|
+
const oldest = this.entries.keys().next().value;
|
|
120
|
+
if (oldest !== undefined)
|
|
121
|
+
this.entries.delete(oldest);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
get size() {
|
|
125
|
+
return this.entries.size;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
101
129
|
// src/proxy/profiles.ts
|
|
102
130
|
var CONFIG_FILE = join(homedir(), ".config", "meridian", "profiles.json");
|
|
103
131
|
var DISK_CACHE_TTL_MS = 5000;
|
|
@@ -213,4 +241,4 @@ function listProfiles(profiles, defaultProfile) {
|
|
|
213
241
|
}));
|
|
214
242
|
}
|
|
215
243
|
|
|
216
|
-
export { getRoutingMode, resolvePriorityOrder, choosePriorityProfile, ProfileExhaustion, loadProfilesFromDisk, setActiveProfile, getActiveProfileId, resetActiveProfile, restoreActiveProfile, enableDiskProfileDiscovery, getEffectiveProfiles, hasProfiles, resolveProfile, listProfiles };
|
|
244
|
+
export { getRoutingMode, resolvePriorityOrder, choosePriorityProfile, ProfileExhaustion, AssignmentStore, loadProfilesFromDisk, setActiveProfile, getActiveProfileId, resetActiveProfile, restoreActiveProfile, enableDiskProfileDiscovery, getEffectiveProfiles, hasProfiles, resolveProfile, listProfiles };
|
|
@@ -235,16 +235,19 @@ async function doRefresh(store) {
|
|
|
235
235
|
}
|
|
236
236
|
const now = Date.now();
|
|
237
237
|
const expiresAt = tokenData.expires_at ?? (tokenData.expires_in ? now + tokenData.expires_in * 1000 : now + 8 * 60 * 60 * 1000);
|
|
238
|
+
const refreshTokenExpiresAtRaw = tokenData.refresh_token_expires_at ?? (tokenData.refresh_token_expires_in ? now + tokenData.refresh_token_expires_in * 1000 : undefined);
|
|
239
|
+
const refreshTokenExpiresAt = refreshTokenExpiresAtRaw && refreshTokenExpiresAtRaw > now ? refreshTokenExpiresAtRaw : undefined;
|
|
238
240
|
credentials.claudeAiOauth = {
|
|
239
241
|
...credentials.claudeAiOauth,
|
|
240
242
|
accessToken: tokenData.access_token,
|
|
241
243
|
refreshToken: tokenData.refresh_token ?? refreshToken,
|
|
242
|
-
expiresAt
|
|
244
|
+
expiresAt,
|
|
245
|
+
...refreshTokenExpiresAt ? { refreshTokenExpiresAt } : {}
|
|
243
246
|
};
|
|
244
247
|
const written = await store.write(credentials);
|
|
245
248
|
if (!written)
|
|
246
249
|
return false;
|
|
247
|
-
claudeLog("token_refresh.success", { expiresAt });
|
|
250
|
+
claudeLog("token_refresh.success", { expiresAt, refreshTokenExpiresAt });
|
|
248
251
|
return true;
|
|
249
252
|
}
|
|
250
253
|
async function ensureFreshToken(store, bufferMs = 5 * 60 * 1000) {
|
|
@@ -257,6 +260,62 @@ async function ensureFreshToken(store, bufferMs = 5 * 60 * 1000) {
|
|
|
257
260
|
return true;
|
|
258
261
|
return refreshOAuthToken(s);
|
|
259
262
|
}
|
|
263
|
+
var DEFAULT_RENEWAL_WARN_DAYS = 3;
|
|
264
|
+
function resolveRenewalWarnDays(raw) {
|
|
265
|
+
const parsed = raw ? Number(raw) : NaN;
|
|
266
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_RENEWAL_WARN_DAYS;
|
|
267
|
+
}
|
|
268
|
+
var RENEWAL_EXPIRY_TTL_MS = 5 * 60000;
|
|
269
|
+
var renewalExpiryCache = new Map;
|
|
270
|
+
var renewalExpiryInflight = new Map;
|
|
271
|
+
function resetAuthRenewalCache() {
|
|
272
|
+
renewalExpiryCache.clear();
|
|
273
|
+
renewalExpiryInflight.clear();
|
|
274
|
+
}
|
|
275
|
+
async function readRefreshTokenExpiry(s) {
|
|
276
|
+
const key = s.refreshKey;
|
|
277
|
+
if (key) {
|
|
278
|
+
const cached = renewalExpiryCache.get(key);
|
|
279
|
+
if (cached && Date.now() - cached.at < RENEWAL_EXPIRY_TTL_MS)
|
|
280
|
+
return cached.value;
|
|
281
|
+
const inflight = renewalExpiryInflight.get(key);
|
|
282
|
+
if (inflight)
|
|
283
|
+
return inflight;
|
|
284
|
+
}
|
|
285
|
+
const read = (async () => {
|
|
286
|
+
let credentials = null;
|
|
287
|
+
try {
|
|
288
|
+
credentials = await s.read();
|
|
289
|
+
} catch {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const value = credentials?.claudeAiOauth?.refreshTokenExpiresAt;
|
|
293
|
+
if (key)
|
|
294
|
+
renewalExpiryCache.set(key, { value, at: Date.now() });
|
|
295
|
+
return value;
|
|
296
|
+
})();
|
|
297
|
+
if (!key)
|
|
298
|
+
return read;
|
|
299
|
+
renewalExpiryInflight.set(key, read);
|
|
300
|
+
try {
|
|
301
|
+
return await read;
|
|
302
|
+
} finally {
|
|
303
|
+
renewalExpiryInflight.delete(key);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async function getAuthRenewalStatus(store, warnDays = DEFAULT_RENEWAL_WARN_DAYS) {
|
|
307
|
+
const s = store ?? createPlatformCredentialStore();
|
|
308
|
+
const refreshTokenExpiresAt = await readRefreshTokenExpiry(s);
|
|
309
|
+
if (!refreshTokenExpiresAt)
|
|
310
|
+
return { renewalRequiredSoon: false };
|
|
311
|
+
const msRemaining = refreshTokenExpiresAt - Date.now();
|
|
312
|
+
const daysUntilRenewal = Math.ceil(msRemaining / 86400000);
|
|
313
|
+
return {
|
|
314
|
+
refreshTokenExpiresAt,
|
|
315
|
+
daysUntilRenewal,
|
|
316
|
+
renewalRequiredSoon: daysUntilRenewal <= warnDays
|
|
317
|
+
};
|
|
318
|
+
}
|
|
260
319
|
var scheduledRefreshTimer = null;
|
|
261
320
|
var scheduledRefreshActive = false;
|
|
262
321
|
var scheduledRefreshGeneration = 0;
|
|
@@ -315,4 +374,4 @@ function resetInflightRefresh() {
|
|
|
315
374
|
inflightRefreshByKey.clear();
|
|
316
375
|
}
|
|
317
376
|
|
|
318
|
-
export { withClaudeLogContext, claudeLog, configDirToKeychainService, configDirToCredentialsFile, serializeCredentials, createPlatformCredentialStore, credentialsFilePathForProfile, refreshOAuthToken, ensureFreshToken, startBackgroundRefresh, stopBackgroundRefresh, isBackgroundRefreshActive, resetInflightRefresh };
|
|
377
|
+
export { withClaudeLogContext, claudeLog, configDirToKeychainService, configDirToCredentialsFile, serializeCredentials, createPlatformCredentialStore, credentialsFilePathForProfile, refreshOAuthToken, ensureFreshToken, DEFAULT_RENEWAL_WARN_DAYS, resolveRenewalWarnDays, resetAuthRenewalCache, getAuthRenewalStatus, startBackgroundRefresh, stopBackgroundRefresh, isBackgroundRefreshActive, resetInflightRefresh };
|