@rynfar/meridian 1.48.0 → 1.49.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 +46 -1
- package/dist/{cli-cx463q74.js → cli-f0yqy2d2.js} +30 -3
- package/dist/{cli-da9hc1s1.js → cli-red47yv3.js} +211 -50
- package/dist/cli.js +3 -3
- package/dist/{profiles-rdd84b45.js → profiles-84hnbd6c.js} +1 -1
- package/dist/proxy/adapter.d.ts +12 -0
- package/dist/proxy/adapter.d.ts.map +1 -1
- package/dist/proxy/adapterInstances.d.ts +63 -0
- package/dist/proxy/adapterInstances.d.ts.map +1 -0
- package/dist/proxy/adapters/detect.d.ts +0 -13
- package/dist/proxy/adapters/detect.d.ts.map +1 -1
- package/dist/proxy/messages.d.ts +15 -4
- package/dist/proxy/messages.d.ts.map +1 -1
- package/dist/proxy/profiles.d.ts +16 -1
- package/dist/proxy/profiles.d.ts.map +1 -1
- package/dist/proxy/routing.d.ts +44 -0
- package/dist/proxy/routing.d.ts.map +1 -0
- package/dist/proxy/server.d.ts.map +1 -1
- package/dist/proxy/settings.d.ts +3 -0
- package/dist/proxy/settings.d.ts.map +1 -1
- package/dist/server.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,6 +99,8 @@ Then in `~/.config/opencode/opencode.json`:
|
|
|
99
99
|
|
|
100
100
|
> **Important:** Do not use `meridian setup` on NixOS. It writes an absolute Nix store path (e.g. `/nix/store/...-meridian-1.x.x/lib/...`) into your OpenCode config, which will break on the next `nixos-rebuild switch` or `home-manager switch` when the store path changes. Use one of the approaches above instead.
|
|
101
101
|
|
|
102
|
+
> **Note:** The bundled Claude Code binary (`claude.exe`) is patched with `autoPatchelfHook` at build time, so it runs on NixOS out of the box. If you previously enabled `programs.nix-ld.enable = true` as a workaround for `Could not start dynamically linked executable` (#501), that is no longer required for Meridian.
|
|
103
|
+
|
|
102
104
|
**Home Manager service** -- run Meridian as a user systemd service:
|
|
103
105
|
|
|
104
106
|
```nix
|
|
@@ -163,7 +165,8 @@ The Claude Code SDK provides programmatic access to Claude. But your favorite co
|
|
|
163
165
|
- **Auto token refresh** — expired OAuth tokens are refreshed automatically; requests continue without interruption
|
|
164
166
|
- **Passthrough mode** — forward tool calls to the client instead of executing internally
|
|
165
167
|
- **Multimodal** — images, documents, file attachments, and multimodal tool results pass through to Claude
|
|
166
|
-
- **Multi-profile** — switch between Claude accounts instantly, no restart needed
|
|
168
|
+
- **Multi-profile** — switch between Claude accounts instantly, no restart needed; opt-in [sticky session routing](#sticky-session-routing) distributes sessions across accounts while keeping per-account prompt caches warm
|
|
169
|
+
- **Adapter instances** — run several configurations of the same adapter side by side (per-instance thinking, system prompt, passthrough) selected by header or match rules — see [Adapter instances](#adapter-instances)
|
|
167
170
|
- **Telemetry dashboard** — real-time performance metrics at `/telemetry`, including token usage and prompt cache efficiency ([`MONITORING.md`](MONITORING.md))
|
|
168
171
|
- **Telemetry persistence** — opt-in SQLite storage for telemetry data that survives proxy restarts, with configurable retention
|
|
169
172
|
- **Prometheus metrics** — `GET /metrics` endpoint for scraping request counters and duration histograms
|
|
@@ -288,6 +291,22 @@ curl -H "x-meridian-profile: work" ...
|
|
|
288
291
|
|
|
289
292
|
You can also switch profiles from the web UI at `http://127.0.0.1:3456/profiles` — a dropdown appears in the nav bar on all pages when profiles are configured.
|
|
290
293
|
|
|
294
|
+
### Sticky session routing
|
|
295
|
+
|
|
296
|
+
With multiple profiles (e.g. two Claude Max subscriptions), Meridian can distribute sessions across profiles automatically while preserving **session affinity** — Anthropic's prompt caching is per-account, so a session must stay on one account to keep its ~99% cache hit rate:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
MERIDIAN_ROUTING=sticky meridian # or set "routing": "sticky" in ~/.config/meridian/settings.json
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
- Each session is assigned to a profile by rendezvous hashing of its session id — **deterministic and stateless**, so assignments survive proxy restarts with no state to lose
|
|
303
|
+
- Adding/removing a profile only reassigns the sessions belonging to the changed arm — everything else keeps its warm cache
|
|
304
|
+
- A session's subagent/fork requests share its assignment (same session id → same account)
|
|
305
|
+
- The `x-meridian-profile` header still overrides everything, per request
|
|
306
|
+
- Default is `active` (all traffic to the active profile — the pre-existing behavior); sticky is opt-in
|
|
307
|
+
|
|
308
|
+
Request logs show the assignment (`profile=work(sticky)`), and `GET /profiles/list` reports the current `routing` mode.
|
|
309
|
+
|
|
291
310
|
### Profile commands
|
|
292
311
|
|
|
293
312
|
| Command | Description |
|
|
@@ -331,6 +350,11 @@ Profile shapes:
|
|
|
331
350
|
|
|
332
351
|
When `MERIDIAN_PROFILES` is set, it takes precedence over disk-configured profiles. When unset, Meridian auto-discovers profiles from `~/.config/meridian/profiles.json` on each request.
|
|
333
352
|
|
|
353
|
+
Related environment variables:
|
|
354
|
+
|
|
355
|
+
- `MERIDIAN_ROUTING=sticky` — enable [sticky session routing](#sticky-session-routing) across profiles (default `active`)
|
|
356
|
+
- `MERIDIAN_ADAPTER_INSTANCES='{...}'` — define [adapter instances](#adapter-instances) inline instead of via `~/.config/meridian/adapter-instances.json`
|
|
357
|
+
|
|
334
358
|
## Agent Setup
|
|
335
359
|
|
|
336
360
|
### OpenCode
|
|
@@ -580,6 +604,27 @@ Requests flow through the Claude Code adapter which:
|
|
|
580
604
|
- Leaves the SDK subprocess cwd on the proxy host (Claude Code's local paths don't exist there).
|
|
581
605
|
- Runs in passthrough mode by default — Claude Code executes its own tools on the machine it runs on; Meridian just forwards tool_use blocks.
|
|
582
606
|
|
|
607
|
+
### Adapter instances
|
|
608
|
+
|
|
609
|
+
Run several configurations of the same adapter side by side — e.g. a passthrough variant with thinking enabled and one without, or a dedicated config for a specific client. Define instances in `~/.config/meridian/adapter-instances.json` (or the `MERIDIAN_ADAPTER_INSTANCES` env var as a JSON string):
|
|
610
|
+
|
|
611
|
+
```jsonc
|
|
612
|
+
{
|
|
613
|
+
"oc-thinky": { "base": "opencode", "features": { "thinking": "enabled" } },
|
|
614
|
+
"lite-plain": { "base": "passthrough", "passthrough": true,
|
|
615
|
+
"match": { "userAgentPrefix": "litellm/" } },
|
|
616
|
+
"team-webui": { "base": "opencode", "features": { "codeSystemPrompt": false },
|
|
617
|
+
"match": { "header": { "x-team": "alpha" } } }
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
- **`base`** — which built-in adapter provides the behavior (tool handling, session tracking, transforms). Existing plugins and transforms scoped to the base adapter apply to its instances automatically.
|
|
622
|
+
- **`features`** — per-instance overrides of the [SDK feature toggles](#sdk-feature-toggles-experimental) (thinking, system prompts, memory, ...) layered over the base's settings. Same keys as the settings UI.
|
|
623
|
+
- **`passthrough`** — per-instance passthrough mode, overriding the adapter default and `MERIDIAN_PASSTHROUGH`.
|
|
624
|
+
- **`match`** — optional automatic selection: exact header values and/or a User-Agent prefix. Match rules outrank built-in User-Agent detection (that's their purpose). Without `match`, select the instance per request with `x-meridian-agent: <instance-name>`.
|
|
625
|
+
|
|
626
|
+
Built-in adapter names are reserved and can't be shadowed. With no instances configured, detection is exactly the built-in chain. Config file changes apply within ~5s, no restart needed.
|
|
627
|
+
|
|
583
628
|
### Any Anthropic-compatible tool
|
|
584
629
|
|
|
585
630
|
```bash
|
|
@@ -7,6 +7,32 @@ import {
|
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
|
+
|
|
11
|
+
// src/proxy/routing.ts
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
function getRoutingMode(raw) {
|
|
14
|
+
return raw?.toLowerCase() === "sticky" ? "sticky" : "active";
|
|
15
|
+
}
|
|
16
|
+
function rendezvousScore(sessionKey, profileId) {
|
|
17
|
+
const digest = createHash("sha256").update(`${sessionKey}\x00${profileId}`).digest();
|
|
18
|
+
return digest.readBigUInt64BE(0);
|
|
19
|
+
}
|
|
20
|
+
function pickStickyProfile(sessionKey, profileIds) {
|
|
21
|
+
if (!sessionKey || profileIds.length === 0)
|
|
22
|
+
return;
|
|
23
|
+
let best;
|
|
24
|
+
let bestScore = -1n;
|
|
25
|
+
for (const id of profileIds) {
|
|
26
|
+
const score = rendezvousScore(sessionKey, id);
|
|
27
|
+
if (score > bestScore) {
|
|
28
|
+
bestScore = score;
|
|
29
|
+
best = id;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return best;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/proxy/profiles.ts
|
|
10
36
|
var CONFIG_FILE = join(homedir(), ".config", "meridian", "profiles.json");
|
|
11
37
|
var DISK_CACHE_TTL_MS = 5000;
|
|
12
38
|
var diskProfilesCache = [];
|
|
@@ -72,12 +98,13 @@ function getEffectiveProfiles(configProfiles) {
|
|
|
72
98
|
function hasProfiles(configProfiles) {
|
|
73
99
|
return getEffectiveProfiles(configProfiles).length > 0;
|
|
74
100
|
}
|
|
75
|
-
function resolveProfile(profiles, defaultProfile, requestedId) {
|
|
101
|
+
function resolveProfile(profiles, defaultProfile, requestedId, options) {
|
|
76
102
|
const effective = getEffectiveProfiles(profiles);
|
|
77
103
|
if (effective.length === 0) {
|
|
78
104
|
return { id: DEFAULT_PROFILE_ID, type: "claude-max", env: {} };
|
|
79
105
|
}
|
|
80
|
-
const
|
|
106
|
+
const stickyId = options?.routingMode === "sticky" && options.stickySessionKey ? pickStickyProfile(options.stickySessionKey, effective.map((p) => p.id)) : undefined;
|
|
107
|
+
const resolvedId = requestedId || stickyId || activeProfileId || defaultProfile || effective[0].id;
|
|
81
108
|
const profile = effective.find((p) => p.id === resolvedId);
|
|
82
109
|
if (!profile) {
|
|
83
110
|
console.warn(`[meridian] Unknown profile "${resolvedId}". Using first configured profile.`);
|
|
@@ -120,4 +147,4 @@ function listProfiles(profiles, defaultProfile) {
|
|
|
120
147
|
}));
|
|
121
148
|
}
|
|
122
149
|
|
|
123
|
-
export { loadProfilesFromDisk, setActiveProfile, getActiveProfileId, resetActiveProfile, restoreActiveProfile, enableDiskProfileDiscovery, getEffectiveProfiles, hasProfiles, resolveProfile, listProfiles };
|
|
150
|
+
export { getRoutingMode, loadProfilesFromDisk, setActiveProfile, getActiveProfileId, resetActiveProfile, restoreActiveProfile, enableDiskProfileDiscovery, getEffectiveProfiles, hasProfiles, resolveProfile, listProfiles };
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getActiveProfileId,
|
|
3
3
|
getEffectiveProfiles,
|
|
4
|
+
getRoutingMode,
|
|
4
5
|
listProfiles,
|
|
5
6
|
resolveProfile,
|
|
6
7
|
restoreActiveProfile,
|
|
7
8
|
setActiveProfile
|
|
8
|
-
} from "./cli-
|
|
9
|
+
} from "./cli-f0yqy2d2.js";
|
|
9
10
|
import {
|
|
10
11
|
isTrackedPlugin,
|
|
11
12
|
recordError,
|
|
@@ -36,6 +37,9 @@ import {
|
|
|
36
37
|
resolveSdkModelDefaults,
|
|
37
38
|
stripExtendedContext
|
|
38
39
|
} from "./cli-q6dz9r5p.js";
|
|
40
|
+
import {
|
|
41
|
+
getSetting
|
|
42
|
+
} from "./cli-340h1chz.js";
|
|
39
43
|
import {
|
|
40
44
|
checkPluginConfigured
|
|
41
45
|
} from "./cli-je60fevk.js";
|
|
@@ -1198,14 +1202,14 @@ __export(exports_sdkFeatures, {
|
|
|
1198
1202
|
getExplicitThinking: () => getExplicitThinking,
|
|
1199
1203
|
getAllFeatureConfigs: () => getAllFeatureConfigs
|
|
1200
1204
|
});
|
|
1201
|
-
import { existsSync as
|
|
1202
|
-
import { join as
|
|
1203
|
-
import { homedir as
|
|
1205
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2, renameSync as renameSync2 } from "node:fs";
|
|
1206
|
+
import { join as join5 } from "node:path";
|
|
1207
|
+
import { homedir as homedir4 } from "node:os";
|
|
1204
1208
|
function getConfigPath() {
|
|
1205
|
-
const dir =
|
|
1206
|
-
if (!
|
|
1209
|
+
const dir = join5(homedir4(), ".config", "meridian");
|
|
1210
|
+
if (!existsSync6(dir))
|
|
1207
1211
|
mkdirSync2(dir, { recursive: true });
|
|
1208
|
-
return
|
|
1212
|
+
return join5(dir, "sdk-features.json");
|
|
1209
1213
|
}
|
|
1210
1214
|
function readConfig() {
|
|
1211
1215
|
const now = Date.now();
|
|
@@ -1213,8 +1217,8 @@ function readConfig() {
|
|
|
1213
1217
|
return cachedConfig;
|
|
1214
1218
|
const path3 = getConfigPath();
|
|
1215
1219
|
try {
|
|
1216
|
-
if (
|
|
1217
|
-
cachedConfig = JSON.parse(
|
|
1220
|
+
if (existsSync6(path3)) {
|
|
1221
|
+
cachedConfig = JSON.parse(readFileSync5(path3, "utf-8"));
|
|
1218
1222
|
} else {
|
|
1219
1223
|
cachedConfig = {};
|
|
1220
1224
|
}
|
|
@@ -3738,8 +3742,8 @@ var serve = (options, listeningListener) => {
|
|
|
3738
3742
|
};
|
|
3739
3743
|
|
|
3740
3744
|
// src/proxy/server.ts
|
|
3741
|
-
import { homedir as
|
|
3742
|
-
import { join as
|
|
3745
|
+
import { homedir as homedir5 } from "node:os";
|
|
3746
|
+
import { join as join6 } from "node:path";
|
|
3743
3747
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
3744
3748
|
|
|
3745
3749
|
// src/proxy/rateLimitStore.ts
|
|
@@ -10168,6 +10172,38 @@ function consolidateMultimodalOntoLastUser(structured) {
|
|
|
10168
10172
|
return result;
|
|
10169
10173
|
}
|
|
10170
10174
|
var TOOL_TARGET_KEYS = ["filePath", "file_path", "path", "command", "pattern", "query", "url"];
|
|
10175
|
+
var CONTENT_SUMMARY_MAX = 120;
|
|
10176
|
+
function summarizeContent(value) {
|
|
10177
|
+
if (typeof value !== "string" || value.length === 0)
|
|
10178
|
+
return;
|
|
10179
|
+
const collapsed = value.replace(/\s+/g, " ").trim();
|
|
10180
|
+
if (!collapsed)
|
|
10181
|
+
return;
|
|
10182
|
+
return collapsed.length > CONTENT_SUMMARY_MAX ? collapsed.slice(0, CONTENT_SUMMARY_MAX) + "..." : collapsed;
|
|
10183
|
+
}
|
|
10184
|
+
function extractContentSummary(name, input) {
|
|
10185
|
+
if (!input || typeof input !== "object")
|
|
10186
|
+
return;
|
|
10187
|
+
const rec = input;
|
|
10188
|
+
switch (name.toLowerCase()) {
|
|
10189
|
+
case "edit":
|
|
10190
|
+
return summarizeContent(rec.newString ?? rec.new_string);
|
|
10191
|
+
case "write":
|
|
10192
|
+
return summarizeContent(rec.content);
|
|
10193
|
+
case "multiedit": {
|
|
10194
|
+
const edits = rec.edits;
|
|
10195
|
+
if (!Array.isArray(edits) || edits.length === 0)
|
|
10196
|
+
return;
|
|
10197
|
+
const first = edits[0];
|
|
10198
|
+
const firstSummary = summarizeContent(first?.newString ?? first?.new_string);
|
|
10199
|
+
if (!firstSummary)
|
|
10200
|
+
return;
|
|
10201
|
+
return edits.length > 1 ? `${edits.length} edits; first: ${firstSummary}` : firstSummary;
|
|
10202
|
+
}
|
|
10203
|
+
default:
|
|
10204
|
+
return;
|
|
10205
|
+
}
|
|
10206
|
+
}
|
|
10171
10207
|
function buildToolUseIndex(messages) {
|
|
10172
10208
|
const index = new Map;
|
|
10173
10209
|
for (const m of messages) {
|
|
@@ -10187,13 +10223,18 @@ function buildToolUseIndex(messages) {
|
|
|
10187
10223
|
}
|
|
10188
10224
|
}
|
|
10189
10225
|
}
|
|
10190
|
-
index.set(block.id, {
|
|
10226
|
+
index.set(block.id, {
|
|
10227
|
+
name: block.name,
|
|
10228
|
+
target,
|
|
10229
|
+
contentSummary: extractContentSummary(block.name, input)
|
|
10230
|
+
});
|
|
10191
10231
|
}
|
|
10192
10232
|
}
|
|
10193
10233
|
return index;
|
|
10194
10234
|
}
|
|
10195
10235
|
function describeToolCall(info) {
|
|
10196
|
-
|
|
10236
|
+
const head = info.target ? `your ${info.name} ${info.target}` : `your ${info.name}`;
|
|
10237
|
+
return info.contentSummary ? `[${head} → "${info.contentSummary}"]` : `[${head}]`;
|
|
10197
10238
|
}
|
|
10198
10239
|
|
|
10199
10240
|
// src/proxy/auth.ts
|
|
@@ -11212,6 +11253,87 @@ var cherryAdapter = {
|
|
|
11212
11253
|
}
|
|
11213
11254
|
};
|
|
11214
11255
|
|
|
11256
|
+
// src/proxy/adapterInstances.ts
|
|
11257
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
|
|
11258
|
+
import { join as join2 } from "node:path";
|
|
11259
|
+
import { homedir as homedir2 } from "node:os";
|
|
11260
|
+
var CONFIG_FILE = join2(homedir2(), ".config", "meridian", "adapter-instances.json");
|
|
11261
|
+
function parseAdapterInstances(raw2) {
|
|
11262
|
+
if (!raw2)
|
|
11263
|
+
return {};
|
|
11264
|
+
let parsed;
|
|
11265
|
+
try {
|
|
11266
|
+
parsed = JSON.parse(raw2);
|
|
11267
|
+
} catch {
|
|
11268
|
+
console.warn("[meridian] adapter-instances config is not valid JSON — ignoring");
|
|
11269
|
+
return {};
|
|
11270
|
+
}
|
|
11271
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
11272
|
+
return {};
|
|
11273
|
+
const out = {};
|
|
11274
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
11275
|
+
const v = value;
|
|
11276
|
+
if (!v || typeof v !== "object" || typeof v.base !== "string" || v.base.length === 0) {
|
|
11277
|
+
console.warn(`[meridian] adapter instance "${name}" has no valid "base" — ignoring`);
|
|
11278
|
+
continue;
|
|
11279
|
+
}
|
|
11280
|
+
if (v.features !== undefined && (typeof v.features !== "object" || v.features === null || Array.isArray(v.features))) {
|
|
11281
|
+
console.warn(`[meridian] adapter instance "${name}" has invalid "features" — ignoring`);
|
|
11282
|
+
continue;
|
|
11283
|
+
}
|
|
11284
|
+
if (v.match !== undefined && (typeof v.match !== "object" || v.match === null || Array.isArray(v.match))) {
|
|
11285
|
+
console.warn(`[meridian] adapter instance "${name}" has invalid "match" — ignoring`);
|
|
11286
|
+
continue;
|
|
11287
|
+
}
|
|
11288
|
+
out[name] = {
|
|
11289
|
+
base: v.base,
|
|
11290
|
+
...v.features ? { features: v.features } : {},
|
|
11291
|
+
...typeof v.passthrough === "boolean" ? { passthrough: v.passthrough } : {},
|
|
11292
|
+
...v.match ? { match: v.match } : {}
|
|
11293
|
+
};
|
|
11294
|
+
}
|
|
11295
|
+
return out;
|
|
11296
|
+
}
|
|
11297
|
+
var DISK_CACHE_TTL_MS = 5000;
|
|
11298
|
+
var diskCache = {};
|
|
11299
|
+
var diskCacheAt = 0;
|
|
11300
|
+
function loadAdapterInstances() {
|
|
11301
|
+
const fromEnv = process.env.MERIDIAN_ADAPTER_INSTANCES;
|
|
11302
|
+
if (fromEnv)
|
|
11303
|
+
return parseAdapterInstances(fromEnv);
|
|
11304
|
+
if (diskCacheAt > 0 && Date.now() - diskCacheAt < DISK_CACHE_TTL_MS)
|
|
11305
|
+
return diskCache;
|
|
11306
|
+
try {
|
|
11307
|
+
diskCache = existsSync3(CONFIG_FILE) ? parseAdapterInstances(readFileSync2(CONFIG_FILE, "utf-8")) : {};
|
|
11308
|
+
} catch (err) {
|
|
11309
|
+
console.warn(`[meridian] Failed to read ${CONFIG_FILE}: ${err instanceof Error ? err.message : err}`);
|
|
11310
|
+
diskCache = {};
|
|
11311
|
+
}
|
|
11312
|
+
diskCacheAt = Date.now();
|
|
11313
|
+
return diskCache;
|
|
11314
|
+
}
|
|
11315
|
+
function matchesInstance(def, getHeader) {
|
|
11316
|
+
const match2 = def.match;
|
|
11317
|
+
if (!match2)
|
|
11318
|
+
return false;
|
|
11319
|
+
const hasHeaderRules = match2.header !== undefined && Object.keys(match2.header).length > 0;
|
|
11320
|
+
const hasUaRule = typeof match2.userAgentPrefix === "string" && match2.userAgentPrefix.length > 0;
|
|
11321
|
+
if (!hasHeaderRules && !hasUaRule)
|
|
11322
|
+
return false;
|
|
11323
|
+
if (hasHeaderRules) {
|
|
11324
|
+
for (const [name, want] of Object.entries(match2.header)) {
|
|
11325
|
+
if (getHeader(name.toLowerCase()) !== want)
|
|
11326
|
+
return false;
|
|
11327
|
+
}
|
|
11328
|
+
}
|
|
11329
|
+
if (hasUaRule) {
|
|
11330
|
+
const ua = getHeader("user-agent") ?? "";
|
|
11331
|
+
if (!ua.startsWith(match2.userAgentPrefix))
|
|
11332
|
+
return false;
|
|
11333
|
+
}
|
|
11334
|
+
return true;
|
|
11335
|
+
}
|
|
11336
|
+
|
|
11215
11337
|
// src/proxy/adapters/detect.ts
|
|
11216
11338
|
var ADAPTER_MAP = {
|
|
11217
11339
|
opencode: openCodeAdapter,
|
|
@@ -11237,11 +11359,41 @@ function isLiteLLMRequest(c) {
|
|
|
11237
11359
|
const headers = c.req.header();
|
|
11238
11360
|
return Object.keys(headers).some((k) => k.toLowerCase().startsWith("x-litellm-"));
|
|
11239
11361
|
}
|
|
11362
|
+
function makeInstanceAdapter(name, def) {
|
|
11363
|
+
const base = ADAPTER_MAP[def.base.toLowerCase()];
|
|
11364
|
+
if (!base) {
|
|
11365
|
+
console.warn(`[meridian] adapter instance "${name}" references unknown base "${def.base}" — ignoring`);
|
|
11366
|
+
return;
|
|
11367
|
+
}
|
|
11368
|
+
return {
|
|
11369
|
+
...base,
|
|
11370
|
+
name,
|
|
11371
|
+
baseName: base.name,
|
|
11372
|
+
...def.features ? { instanceFeatures: def.features } : {},
|
|
11373
|
+
...def.passthrough !== undefined ? { instancePassthrough: def.passthrough } : {}
|
|
11374
|
+
};
|
|
11375
|
+
}
|
|
11240
11376
|
function detectAdapter(c) {
|
|
11241
11377
|
const agentOverride = c.req.header("x-meridian-agent")?.toLowerCase();
|
|
11242
11378
|
if (agentOverride && ADAPTER_MAP[agentOverride]) {
|
|
11243
11379
|
return ADAPTER_MAP[agentOverride];
|
|
11244
11380
|
}
|
|
11381
|
+
const instances = loadAdapterInstances();
|
|
11382
|
+
const instanceNames = Object.keys(instances);
|
|
11383
|
+
if (instanceNames.length > 0) {
|
|
11384
|
+
if (agentOverride && instances[agentOverride]) {
|
|
11385
|
+
const inst = makeInstanceAdapter(agentOverride, instances[agentOverride]);
|
|
11386
|
+
if (inst)
|
|
11387
|
+
return inst;
|
|
11388
|
+
}
|
|
11389
|
+
for (const name of instanceNames) {
|
|
11390
|
+
if (matchesInstance(instances[name], (h) => c.req.header(h))) {
|
|
11391
|
+
const inst = makeInstanceAdapter(name, instances[name]);
|
|
11392
|
+
if (inst)
|
|
11393
|
+
return inst;
|
|
11394
|
+
}
|
|
11395
|
+
}
|
|
11396
|
+
}
|
|
11245
11397
|
if (c.req.header("x-opencode-session") || c.req.header("x-session-affinity")) {
|
|
11246
11398
|
return openCodeAdapter;
|
|
11247
11399
|
}
|
|
@@ -17182,8 +17334,8 @@ function getAdapterTransforms(adapterName) {
|
|
|
17182
17334
|
}
|
|
17183
17335
|
|
|
17184
17336
|
// src/proxy/plugins/loader.ts
|
|
17185
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
17186
|
-
import { join as
|
|
17337
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
|
|
17338
|
+
import { join as join3, isAbsolute as isAbsolute2, extname } from "path";
|
|
17187
17339
|
import { pathToFileURL } from "url";
|
|
17188
17340
|
|
|
17189
17341
|
// src/proxy/plugins/validation.ts
|
|
@@ -17224,10 +17376,10 @@ function validateTransform(exported) {
|
|
|
17224
17376
|
// src/proxy/plugins/loader.ts
|
|
17225
17377
|
var loadCounter = 0;
|
|
17226
17378
|
function parsePluginConfig(configPath) {
|
|
17227
|
-
if (!
|
|
17379
|
+
if (!existsSync4(configPath))
|
|
17228
17380
|
return [];
|
|
17229
17381
|
try {
|
|
17230
|
-
const raw2 =
|
|
17382
|
+
const raw2 = readFileSync3(configPath, "utf-8");
|
|
17231
17383
|
const parsed = JSON.parse(raw2);
|
|
17232
17384
|
return Array.isArray(parsed.plugins) ? parsed.plugins : [];
|
|
17233
17385
|
} catch {
|
|
@@ -17237,7 +17389,7 @@ function parsePluginConfig(configPath) {
|
|
|
17237
17389
|
async function loadPlugins(pluginDir, configPath) {
|
|
17238
17390
|
resetAllPluginStats();
|
|
17239
17391
|
const config = configPath ? parsePluginConfig(configPath) : [];
|
|
17240
|
-
const pluginDirExists =
|
|
17392
|
+
const pluginDirExists = existsSync4(pluginDir);
|
|
17241
17393
|
let filenames = [];
|
|
17242
17394
|
if (pluginDirExists) {
|
|
17243
17395
|
try {
|
|
@@ -17268,7 +17420,7 @@ async function loadPlugins(pluginDir, configPath) {
|
|
|
17268
17420
|
const loaded = [];
|
|
17269
17421
|
const seenNames = new Set;
|
|
17270
17422
|
for (const { filename, entry } of ordered) {
|
|
17271
|
-
const filePath = isAbsolute2(filename) ? filename :
|
|
17423
|
+
const filePath = isAbsolute2(filename) ? filename : join3(pluginDir, filename);
|
|
17272
17424
|
if (entry && !entry.enabled) {
|
|
17273
17425
|
loaded.push({
|
|
17274
17426
|
name: filename,
|
|
@@ -17641,17 +17793,17 @@ function verifyLineage(cached, messages, cacheKey2, cache) {
|
|
|
17641
17793
|
// src/proxy/sessionStore.ts
|
|
17642
17794
|
import {
|
|
17643
17795
|
closeSync,
|
|
17644
|
-
existsSync as
|
|
17796
|
+
existsSync as existsSync5,
|
|
17645
17797
|
mkdirSync,
|
|
17646
17798
|
openSync,
|
|
17647
|
-
readFileSync as
|
|
17799
|
+
readFileSync as readFileSync4,
|
|
17648
17800
|
renameSync,
|
|
17649
17801
|
statSync,
|
|
17650
17802
|
unlinkSync,
|
|
17651
17803
|
writeFileSync
|
|
17652
17804
|
} from "node:fs";
|
|
17653
|
-
import { homedir as
|
|
17654
|
-
import { join as
|
|
17805
|
+
import { homedir as homedir3 } from "node:os";
|
|
17806
|
+
import { join as join4 } from "node:path";
|
|
17655
17807
|
var DEFAULT_MAX_STORED_SESSIONS = 1e4;
|
|
17656
17808
|
var STALE_LOCK_THRESHOLD_MS = 30000;
|
|
17657
17809
|
function getMaxStoredSessions() {
|
|
@@ -17699,17 +17851,17 @@ var sessionDirOverride = null;
|
|
|
17699
17851
|
var skipLocking = false;
|
|
17700
17852
|
function getStorePath() {
|
|
17701
17853
|
const dir = sessionDirOverride || process.env.MERIDIAN_SESSION_DIR || process.env.CLAUDE_PROXY_SESSION_DIR || getDefaultCacheDir();
|
|
17702
|
-
if (!
|
|
17854
|
+
if (!existsSync5(dir)) {
|
|
17703
17855
|
mkdirSync(dir, { recursive: true });
|
|
17704
17856
|
}
|
|
17705
|
-
return
|
|
17857
|
+
return join4(dir, "sessions.json");
|
|
17706
17858
|
}
|
|
17707
17859
|
function getDefaultCacheDir() {
|
|
17708
|
-
const newDir =
|
|
17709
|
-
const oldDir =
|
|
17710
|
-
if (
|
|
17860
|
+
const newDir = join4(homedir3(), ".cache", "meridian");
|
|
17861
|
+
const oldDir = join4(homedir3(), ".cache", "opencode-claude-max-proxy");
|
|
17862
|
+
if (existsSync5(newDir))
|
|
17711
17863
|
return newDir;
|
|
17712
|
-
if (
|
|
17864
|
+
if (existsSync5(oldDir)) {
|
|
17713
17865
|
try {
|
|
17714
17866
|
const { symlinkSync } = __require("fs");
|
|
17715
17867
|
symlinkSync(oldDir, newDir);
|
|
@@ -17722,10 +17874,10 @@ function getDefaultCacheDir() {
|
|
|
17722
17874
|
}
|
|
17723
17875
|
function readStore() {
|
|
17724
17876
|
const path3 = getStorePath();
|
|
17725
|
-
if (!
|
|
17877
|
+
if (!existsSync5(path3))
|
|
17726
17878
|
return {};
|
|
17727
17879
|
try {
|
|
17728
|
-
const data =
|
|
17880
|
+
const data = readFileSync4(path3, "utf-8");
|
|
17729
17881
|
return JSON.parse(data);
|
|
17730
17882
|
} catch (e) {
|
|
17731
17883
|
console.error("[sessionStore] read failed:", e.message);
|
|
@@ -18188,9 +18340,11 @@ function buildFreshPrompt(messages, sanitizeOpts = {}) {
|
|
|
18188
18340
|
}();
|
|
18189
18341
|
}
|
|
18190
18342
|
return messages.map((m) => {
|
|
18191
|
-
|
|
18192
|
-
|
|
18193
|
-
|
|
18343
|
+
if (m.role === "assistant") {
|
|
18344
|
+
const assistantText = flattenAssistantContent(m.content);
|
|
18345
|
+
return assistantText ? `[Assistant: ${assistantText}]` : "";
|
|
18346
|
+
}
|
|
18347
|
+
return flattenUserContent(m.content, sanitizeOpts, toolIndex);
|
|
18194
18348
|
}).filter(Boolean).join(`
|
|
18195
18349
|
|
|
18196
18350
|
`) || "";
|
|
@@ -18254,8 +18408,8 @@ function createProxyServer(config = {}) {
|
|
|
18254
18408
|
const sessionDiscoveredTools = new Map;
|
|
18255
18409
|
const sessionToolCache = new Map;
|
|
18256
18410
|
const sessionMcpCache = new LRUMap(getMaxSessionsLimit());
|
|
18257
|
-
const pluginDir = finalConfig.pluginDir ??
|
|
18258
|
-
const pluginConfigPath = finalConfig.pluginConfigPath ??
|
|
18411
|
+
const pluginDir = finalConfig.pluginDir ?? join6(homedir5(), ".config", "meridian", "plugins");
|
|
18412
|
+
const pluginConfigPath = finalConfig.pluginConfigPath ?? join6(homedir5(), ".config", "meridian", "plugins.json");
|
|
18259
18413
|
let loadedPlugins = [];
|
|
18260
18414
|
let pluginTransforms = [];
|
|
18261
18415
|
const app = new Hono2;
|
|
@@ -18337,7 +18491,8 @@ function createProxyServer(config = {}) {
|
|
|
18337
18491
|
return c.json({ type: "error", error: { type: "invalid_request_error", message: parsedOutputFormat.message } }, 400);
|
|
18338
18492
|
}
|
|
18339
18493
|
const outputFormat = parsedOutputFormat.value;
|
|
18340
|
-
const
|
|
18494
|
+
const routingMode = getRoutingMode(process.env.MERIDIAN_ROUTING ?? getSetting("routing"));
|
|
18495
|
+
const profile = resolveProfile(finalConfig.profiles, finalConfig.defaultProfile, c.req.header("x-meridian-profile") || undefined, routingMode === "sticky" ? { routingMode, stickySessionKey: adapter.getSessionId(c, body) } : undefined);
|
|
18341
18496
|
const authStatus = await getClaudeAuthStatusAsync(profile.id !== "default" ? profile.id : undefined, Object.keys(profile.env).length > 0 ? profile.env : undefined);
|
|
18342
18497
|
const agentMode = c.req.header("x-opencode-agent-mode") ?? null;
|
|
18343
18498
|
const requestSource = c.req.header("x-meridian-source")?.slice(0, 64) || undefined;
|
|
@@ -18377,10 +18532,11 @@ function createProxyServer(config = {}) {
|
|
|
18377
18532
|
`);
|
|
18378
18533
|
}
|
|
18379
18534
|
}
|
|
18380
|
-
const
|
|
18535
|
+
const adapterBase = adapter.baseName ?? adapter.name;
|
|
18536
|
+
const adapterTransforms = getAdapterTransforms(adapterBase);
|
|
18381
18537
|
const pipeline = buildPipeline(adapterTransforms, pluginTransforms);
|
|
18382
18538
|
const pipelineCtx = runTransformHook(pipeline, "onRequest", createRequestContext({
|
|
18383
|
-
adapter:
|
|
18539
|
+
adapter: adapterBase,
|
|
18384
18540
|
body,
|
|
18385
18541
|
headers: c.req.raw.headers,
|
|
18386
18542
|
model,
|
|
@@ -18389,7 +18545,7 @@ function createProxyServer(config = {}) {
|
|
|
18389
18545
|
tools: body.tools,
|
|
18390
18546
|
stream: body.stream ?? false,
|
|
18391
18547
|
workingDirectory
|
|
18392
|
-
}),
|
|
18548
|
+
}), adapterBase);
|
|
18393
18549
|
const stream2 = pipelineCtx.prefersStreaming !== undefined ? pipelineCtx.prefersStreaming : body.stream ?? false;
|
|
18394
18550
|
const effortHeader = c.req.header("x-opencode-effort");
|
|
18395
18551
|
const thinkingHeader = c.req.header("x-opencode-thinking");
|
|
@@ -18409,8 +18565,8 @@ function createProxyServer(config = {}) {
|
|
|
18409
18565
|
}
|
|
18410
18566
|
}
|
|
18411
18567
|
const { getFeaturesForAdapter: getFeaturesForAdapter2, getExplicitThinking: getExplicitThinking2 } = (init_sdkFeatures(), __toCommonJS(exports_sdkFeatures));
|
|
18412
|
-
const sdkFeatures = getFeaturesForAdapter2(adapter.
|
|
18413
|
-
if (
|
|
18568
|
+
const sdkFeatures = { ...getFeaturesForAdapter2(adapterBase), ...adapter.instanceFeatures ?? {} };
|
|
18569
|
+
if ((adapter.instanceFeatures?.thinking ?? getExplicitThinking2(adapterBase)) === "disabled") {
|
|
18414
18570
|
thinking = { type: "disabled" };
|
|
18415
18571
|
effort = undefined;
|
|
18416
18572
|
plog(`[PROXY] ${requestMeta.requestId} thinking disabled (per-adapter setting)`);
|
|
@@ -18436,10 +18592,10 @@ function createProxyServer(config = {}) {
|
|
|
18436
18592
|
const profileScopedCwd = profile.id !== "default" ? `${clientWorkingDirectory}::profile=${profile.id}` : clientWorkingDirectory;
|
|
18437
18593
|
const lastMessage = Array.isArray(body.messages) ? body.messages[body.messages.length - 1] : undefined;
|
|
18438
18594
|
const lastIsToolResult = Array.isArray(lastMessage?.content) && lastMessage.content.some((b) => b?.type === "tool_result");
|
|
18439
|
-
const isClientDrivenLoop =
|
|
18595
|
+
const isClientDrivenLoop = adapterBase !== "claude-code" && !agentSessionId && lastIsToolResult;
|
|
18440
18596
|
const isIndependentSession = requestSource?.startsWith("fork-") || requestSource?.startsWith("subagent-") || isClientDrivenLoop || false;
|
|
18441
18597
|
let lineageResult = isIndependentSession ? { type: "diverged" } : lookupSession(profileSessionId, body.messages || [], profileScopedCwd);
|
|
18442
|
-
if (lineageResult.type === "undo" &&
|
|
18598
|
+
if (lineageResult.type === "undo" && adapterBase === "opencode" && !agentSessionId) {
|
|
18443
18599
|
lineageResult = { type: "diverged" };
|
|
18444
18600
|
}
|
|
18445
18601
|
const isResume = lineageResult.type === "continuation" || lineageResult.type === "compaction";
|
|
@@ -18454,7 +18610,7 @@ function createProxyServer(config = {}) {
|
|
|
18454
18610
|
const lineageType = lineageResult.type === "diverged" && !cachedSession ? "new" : lineageResult.type;
|
|
18455
18611
|
const msgCount = Array.isArray(body.messages) ? body.messages.length : 0;
|
|
18456
18612
|
const toolCount = body.tools?.length ?? 0;
|
|
18457
|
-
const requestLogLine = `${requestMeta.requestId} adapter=${adapter.name}${requestSource ? ` source=${requestSource}` : ""} model=${model} stream=${stream2} tools=${toolCount} lineage=${lineageType} session=${resumeSessionId?.slice(0, 8) || "new"}${isUndo && undoRollbackUuid ? ` rollback=${undoRollbackUuid.slice(0, 8)}` : ""}${agentMode ? ` agent=${agentMode}` : ""} active=${activeSessions}/${MAX_CONCURRENT_SESSIONS} msgCount=${msgCount}`;
|
|
18613
|
+
const requestLogLine = `${requestMeta.requestId} adapter=${adapter.name}${requestSource ? ` source=${requestSource}` : ""}${profile.id !== "default" ? ` profile=${profile.id}${routingMode === "sticky" ? "(sticky)" : ""}` : ""} model=${model} stream=${stream2} tools=${toolCount} lineage=${lineageType} session=${resumeSessionId?.slice(0, 8) || "new"}${isUndo && undoRollbackUuid ? ` rollback=${undoRollbackUuid.slice(0, 8)}` : ""}${agentMode ? ` agent=${agentMode}` : ""} active=${activeSessions}/${MAX_CONCURRENT_SESSIONS} msgCount=${msgCount}`;
|
|
18458
18614
|
plog(`[PROXY] ${requestLogLine} msgs=${msgSummary}`);
|
|
18459
18615
|
diagnosticLog2.session(`${requestLogLine}`, requestMeta.requestId);
|
|
18460
18616
|
if (lineageResult.type === "diverged" && profileSessionId && !isIndependentSession) {
|
|
@@ -18541,14 +18697,18 @@ function createProxyServer(config = {}) {
|
|
|
18541
18697
|
} else {
|
|
18542
18698
|
const toolIndex = buildToolUseIndex(allMessages ?? messagesToConvert ?? []);
|
|
18543
18699
|
textPrompt = messagesToConvert?.map((m) => {
|
|
18544
|
-
|
|
18545
|
-
|
|
18546
|
-
|
|
18700
|
+
if (m.role === "assistant") {
|
|
18701
|
+
if (isResume)
|
|
18702
|
+
return "";
|
|
18703
|
+
const assistantText = flattenAssistantContent(m.content);
|
|
18704
|
+
return assistantText ? `[Assistant: ${assistantText}]` : "";
|
|
18705
|
+
}
|
|
18706
|
+
return flattenUserContent(m.content, sanitizeOpts, toolIndex);
|
|
18547
18707
|
}).filter(Boolean).join(`
|
|
18548
18708
|
|
|
18549
18709
|
`) || "";
|
|
18550
18710
|
}
|
|
18551
|
-
const passthrough = pipelineCtx.passthrough !== undefined ? pipelineCtx.passthrough : envBool("PASSTHROUGH");
|
|
18711
|
+
const passthrough = adapter.instancePassthrough !== undefined ? adapter.instancePassthrough : pipelineCtx.passthrough !== undefined ? pipelineCtx.passthrough : envBool("PASSTHROUGH");
|
|
18552
18712
|
const settingSources = envBool("LOAD_CONTEXT") || sdkFeatures.claudeMd === "full" ? ["user", "project"] : sdkFeatures.claudeMd === "project" ? ["project"] : pipelineCtx.settingSources ?? [];
|
|
18553
18713
|
const capturedToolUses = [];
|
|
18554
18714
|
const capturedSignatures = new Set;
|
|
@@ -20283,7 +20443,8 @@ data: ${JSON.stringify({
|
|
|
20283
20443
|
}));
|
|
20284
20444
|
return c.json({
|
|
20285
20445
|
profiles: enriched,
|
|
20286
|
-
activeProfile: getActiveProfileId() || finalConfig.defaultProfile || profiles[0]?.id || "default"
|
|
20446
|
+
activeProfile: getActiveProfileId() || finalConfig.defaultProfile || profiles[0]?.id || "default",
|
|
20447
|
+
routing: getRoutingMode(process.env.MERIDIAN_ROUTING ?? getSetting("routing"))
|
|
20287
20448
|
});
|
|
20288
20449
|
});
|
|
20289
20450
|
app.get("/profiles", async (c) => {
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
startProxyServer
|
|
4
|
-
} from "./cli-
|
|
5
|
-
import"./cli-
|
|
4
|
+
} from "./cli-red47yv3.js";
|
|
5
|
+
import"./cli-f0yqy2d2.js";
|
|
6
6
|
import"./cli-sry5aqdj.js";
|
|
7
7
|
import"./cli-4rqtm83g.js";
|
|
8
8
|
import {
|
|
@@ -167,7 +167,7 @@ async function runCli(start = startProxyServer, runAuthCheck = async () => {
|
|
|
167
167
|
console.error("\x1B[33m⚠ Could not verify Claude auth status. If requests fail, run: claude login\x1B[0m");
|
|
168
168
|
}
|
|
169
169
|
if (!profiles) {
|
|
170
|
-
const { enableDiskProfileDiscovery } = await import("./profiles-
|
|
170
|
+
const { enableDiskProfileDiscovery } = await import("./profiles-84hnbd6c.js");
|
|
171
171
|
enableDiskProfileDiscovery();
|
|
172
172
|
}
|
|
173
173
|
const proxy = await start({ port, host, idleTimeoutSeconds, profiles, defaultProfile, version, installProcessErrorHandlers: true });
|
package/dist/proxy/adapter.d.ts
CHANGED
|
@@ -67,6 +67,18 @@ export interface AgentIdentity {
|
|
|
67
67
|
* how to interact with the calling agent.
|
|
68
68
|
*/
|
|
69
69
|
export interface AgentAdapter extends AgentIdentity {
|
|
70
|
+
/**
|
|
71
|
+
* For adapter INSTANCES (#476): the base adapter's name. Behavior keyed by
|
|
72
|
+
* adapter name — transforms, plugin scoping, agent-specific branches —
|
|
73
|
+
* must resolve via `baseName ?? name` so existing transforms/plugins keep
|
|
74
|
+
* applying to instances. Features resolve by instance definition instead.
|
|
75
|
+
* Undefined for built-in adapters.
|
|
76
|
+
*/
|
|
77
|
+
readonly baseName?: string;
|
|
78
|
+
/** Instance feature overrides, layered over the base's resolved features. */
|
|
79
|
+
readonly instanceFeatures?: Partial<import("./sdkFeatures").AdapterFeatures>;
|
|
80
|
+
/** Instance passthrough override — beats the adapter transform's default. */
|
|
81
|
+
readonly instancePassthrough?: boolean;
|
|
70
82
|
/**
|
|
71
83
|
* SDK built-in tools to block (replaced by MCP equivalents).
|
|
72
84
|
* These are tools where the agent provides its own implementation.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/proxy/adapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAA;AAEnE;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB;;;OAGG;IACH,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;IAE5D;;;;;;;;;;;;;;;;OAgBG;IACH,uBAAuB,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAAA;IAEtD;;;;;;;;;;;OAWG;IACH,6BAA6B,CAAC,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAAA;IAE7D;;;OAGG;IACH,gBAAgB,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,CAAA;IAEtC;;;OAGG;IACH,gBAAgB,IAAI,MAAM,CAAA;CAC3B;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAa,SAAQ,aAAa;IACjD;;;OAGG;IACH,sBAAsB,IAAI,SAAS,MAAM,EAAE,CAAA;IAE3C;;;;OAIG;IACH,yBAAyB,IAAI,SAAS,MAAM,EAAE,CAAA;IAE9C;;OAEG;IACH,kBAAkB,IAAI,SAAS,MAAM,EAAE,CAAA;IAEvC;;;;OAIG;IACH,cAAc,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAEhF;;;OAGG;IACH,aAAa,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,CAAA;IAE9D;;;OAGG;IACH,0BAA0B,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAA;IAE9E;;;;;;OAMG;IACH,gBAAgB,CAAC,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAA;IAErC;;;;;;;;OAQG;IACH,eAAe,CAAC,IAAI,OAAO,CAAA;IAE3B;;;;;;;OAOG;IACH,gBAAgB,CAAC,IAAI,SAAS,MAAM,EAAE,CAAA;IAEtC;;;;;;;;;;;;;;OAcG;IACH,iBAAiB,CAAC,IAAI,aAAa,EAAE,CAAA;IAErC;;;;;;;OAOG;IACH,gBAAgB,CAAC,IAAI,OAAO,CAAA;IAE5B;;;;;;OAMG;IACH,sBAAsB,CAAC,IAAI,OAAO,CAAA;IAElC;;;;;;;;OAQG;IACH,yBAAyB,CAAC,IAAI,OAAO,CAAA;IAErC;;;;;;;;;;;;;;;OAeG;IACH,6BAA6B,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,OAAO,eAAe,EAAE,UAAU,EAAE,CAAA;CAC3G"}
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/proxy/adapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAA;AAEnE;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB;;;OAGG;IACH,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;IAE5D;;;;;;;;;;;;;;;;OAgBG;IACH,uBAAuB,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAAA;IAEtD;;;;;;;;;;;OAWG;IACH,6BAA6B,CAAC,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAAA;IAE7D;;;OAGG;IACH,gBAAgB,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,CAAA;IAEtC;;;OAGG;IACH,gBAAgB,IAAI,MAAM,CAAA;CAC3B;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAa,SAAQ,aAAa;IACjD;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAE1B,6EAA6E;IAC7E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC,OAAO,eAAe,EAAE,eAAe,CAAC,CAAA;IAE5E,6EAA6E;IAC7E,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAEtC;;;OAGG;IACH,sBAAsB,IAAI,SAAS,MAAM,EAAE,CAAA;IAE3C;;;;OAIG;IACH,yBAAyB,IAAI,SAAS,MAAM,EAAE,CAAA;IAE9C;;OAEG;IACH,kBAAkB,IAAI,SAAS,MAAM,EAAE,CAAA;IAEvC;;;;OAIG;IACH,cAAc,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAEhF;;;OAGG;IACH,aAAa,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,CAAA;IAE9D;;;OAGG;IACH,0BAA0B,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAA;IAE9E;;;;;;OAMG;IACH,gBAAgB,CAAC,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAA;IAErC;;;;;;;;OAQG;IACH,eAAe,CAAC,IAAI,OAAO,CAAA;IAE3B;;;;;;;OAOG;IACH,gBAAgB,CAAC,IAAI,SAAS,MAAM,EAAE,CAAA;IAEtC;;;;;;;;;;;;;;OAcG;IACH,iBAAiB,CAAC,IAAI,aAAa,EAAE,CAAA;IAErC;;;;;;;OAOG;IACH,gBAAgB,CAAC,IAAI,OAAO,CAAA;IAE5B;;;;;;OAMG;IACH,sBAAsB,CAAC,IAAI,OAAO,CAAA;IAElC;;;;;;;;OAQG;IACH,yBAAyB,CAAC,IAAI,OAAO,CAAA;IAErC;;;;;;;;;;;;;;;OAeG;IACH,6BAA6B,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,OAAO,eAAe,EAAE,UAAU,EAAE,CAAA;CAC3G"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter instances (#476, proposed by @Serpentiel).
|
|
3
|
+
*
|
|
4
|
+
* An instance is a named configuration of a base adapter:
|
|
5
|
+
*
|
|
6
|
+
* {
|
|
7
|
+
* "oc-thinky": { "base": "opencode", "features": { "thinking": "enabled" } },
|
|
8
|
+
* "lite-plain": { "base": "passthrough", "passthrough": true,
|
|
9
|
+
* "match": { "userAgentPrefix": "litellm/" } }
|
|
10
|
+
* }
|
|
11
|
+
*
|
|
12
|
+
* Selected explicitly via `x-meridian-agent: <instance-name>`, or
|
|
13
|
+
* automatically via match rules (exact header values and/or a User-Agent
|
|
14
|
+
* prefix). Instances resolve BEHAVIOR (transforms, plugin scoping, session
|
|
15
|
+
* handling) by their base adapter's name and FEATURES by their own
|
|
16
|
+
* definition — see detect.ts and server.ts for the resolution invariant.
|
|
17
|
+
*
|
|
18
|
+
* Config sources (first wins):
|
|
19
|
+
* 1. MERIDIAN_ADAPTER_INSTANCES env var (JSON string)
|
|
20
|
+
* 2. ~/.config/meridian/adapter-instances.json (5s TTL cache)
|
|
21
|
+
*
|
|
22
|
+
* With no instances configured, adapter detection is byte-identical to the
|
|
23
|
+
* built-in chain. Built-in adapter names cannot be shadowed.
|
|
24
|
+
*
|
|
25
|
+
* This is a leaf module — no imports from server.ts, adapters, or session/.
|
|
26
|
+
*/
|
|
27
|
+
import type { AdapterFeatures } from "./sdkFeatures";
|
|
28
|
+
export interface AdapterInstanceMatch {
|
|
29
|
+
/** Exact header values — ALL listed headers must match (case-insensitive names). */
|
|
30
|
+
header?: Record<string, string>;
|
|
31
|
+
/** User-Agent prefix match. */
|
|
32
|
+
userAgentPrefix?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface AdapterInstanceDef {
|
|
35
|
+
/** Base adapter name (opencode, passthrough, crush, ...). Behavior comes from here. */
|
|
36
|
+
base: string;
|
|
37
|
+
/** Per-instance feature overrides, layered over the base's resolved features. */
|
|
38
|
+
features?: Partial<AdapterFeatures>;
|
|
39
|
+
/** Per-instance passthrough override — beats the adapter transform's default. */
|
|
40
|
+
passthrough?: boolean;
|
|
41
|
+
/** Automatic selection rules. Omit to select only via x-meridian-agent. */
|
|
42
|
+
match?: AdapterInstanceMatch;
|
|
43
|
+
}
|
|
44
|
+
export type AdapterInstanceMap = Record<string, AdapterInstanceDef>;
|
|
45
|
+
/**
|
|
46
|
+
* Parse and validate an instance map from raw JSON. Malformed input and
|
|
47
|
+
* invalid entries are dropped (with a warning) rather than crashing —
|
|
48
|
+
* a config typo must never take down adapter detection.
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseAdapterInstances(raw: string | undefined): AdapterInstanceMap;
|
|
51
|
+
/**
|
|
52
|
+
* Load the configured instance map. Env var wins over the disk file.
|
|
53
|
+
* Returns {} when nothing is configured — the common case, and the
|
|
54
|
+
* guarantee that unconfigured setups keep byte-identical detection.
|
|
55
|
+
*/
|
|
56
|
+
export declare function loadAdapterInstances(): AdapterInstanceMap;
|
|
57
|
+
/**
|
|
58
|
+
* Does a request match an instance's rules? No rules = never auto-matched
|
|
59
|
+
* (explicit x-meridian-agent selection only). When both header and UA rules
|
|
60
|
+
* are present, both must match.
|
|
61
|
+
*/
|
|
62
|
+
export declare function matchesInstance(def: AdapterInstanceDef, getHeader: (name: string) => string | undefined): boolean;
|
|
63
|
+
//# sourceMappingURL=adapterInstances.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapterInstances.d.ts","sourceRoot":"","sources":["../../src/proxy/adapterInstances.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAIpD,MAAM,WAAW,oBAAoB;IACnC,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,+BAA+B;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAA;IACZ,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAA;IACnC,iFAAiF;IACjF,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,oBAAoB,CAAA;CAC7B;AAED,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAA;AAEnE;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,kBAAkB,CAkCjF;AAOD;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,kBAAkB,CAazD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,kBAAkB,EACvB,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAC9C,OAAO,CAiBT"}
|
|
@@ -6,18 +6,5 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { Context } from "hono";
|
|
8
8
|
import type { AgentAdapter } from "../adapter";
|
|
9
|
-
/**
|
|
10
|
-
* Detect which agent adapter to use based on request headers.
|
|
11
|
-
*
|
|
12
|
-
* Detection rules (evaluated in order):
|
|
13
|
-
* 1. x-meridian-agent header → explicit adapter override
|
|
14
|
-
* 2. x-opencode-session or x-session-affinity header → OpenCode adapter
|
|
15
|
-
* 3. User-Agent starts with "opencode/" → OpenCode adapter
|
|
16
|
-
* 4. User-Agent starts with "factory-cli/" → Droid adapter
|
|
17
|
-
* 5. User-Agent starts with "Charm-Crush/" → Crush adapter
|
|
18
|
-
* 6. User-Agent starts with "claude-cli/" → Claude Code adapter
|
|
19
|
-
* 7. litellm/* UA or x-litellm-* headers → LiteLLM passthrough adapter
|
|
20
|
-
* 8. Default → MERIDIAN_DEFAULT_AGENT env var, or OpenCode
|
|
21
|
-
*/
|
|
22
9
|
export declare function detectAdapter(c: Context): AgentAdapter;
|
|
23
10
|
//# sourceMappingURL=detect.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../../src/proxy/adapters/detect.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../../src/proxy/adapters/detect.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAsF9C,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,YAAY,CAsEtD"}
|
package/dist/proxy/messages.d.ts
CHANGED
|
@@ -81,6 +81,15 @@ export interface ToolCallInfo {
|
|
|
81
81
|
name: string;
|
|
82
82
|
/** Compact human-readable target (file path, command, pattern...), if any. */
|
|
83
83
|
target: string | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* For mutating tools (edit/write/multiedit): a truncated summary of the
|
|
86
|
+
* content the call produced. On fresh replay the assistant's tool_use
|
|
87
|
+
* blocks are dropped, so without this the model knows THAT it edited a
|
|
88
|
+
* file but not WHAT it wrote — it then re-derives near-duplicate edits
|
|
89
|
+
* and confabulates a parallel editor when it doesn't recognize its own
|
|
90
|
+
* wording (#496 follow-up, stefanpartheym's transcript).
|
|
91
|
+
*/
|
|
92
|
+
contentSummary: string | undefined;
|
|
84
93
|
}
|
|
85
94
|
/**
|
|
86
95
|
* Index every assistant tool_use block in a conversation by id.
|
|
@@ -98,10 +107,12 @@ export declare function buildToolUseIndex(messages: Array<{
|
|
|
98
107
|
}>): Map<string, ToolCallInfo>;
|
|
99
108
|
/**
|
|
100
109
|
* Render a compact attribution label for a replayed tool result, e.g.
|
|
101
|
-
* `[write tmp/test2.txt]
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
110
|
+
* `[write tmp/test2.txt]`, `[bash echo hi]`, or — for mutating tools —
|
|
111
|
+
* `[edit a.yml → "# Ignore major bumps..."]` so the model can recognize its
|
|
112
|
+
* own prior work product on replay. Deliberately terse and bracket-formatted
|
|
113
|
+
* differently from the banned `[Tool Use: ...]` / `[Tool Result ...]` shapes
|
|
114
|
+
* (#111/#386) so the model reads it as context, not as tool-call syntax to
|
|
115
|
+
* imitate.
|
|
105
116
|
*/
|
|
106
117
|
export declare function describeToolCall(info: ToolCallInfo): string;
|
|
107
118
|
//# sourceMappingURL=messages.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/proxy/messages.ts"],"names":[],"mappings":"AAAA;;GAEG;AAcH;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,CAiBrD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAUtE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAM7D;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,CAKzH;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,4BAA4B,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAU3D;AAED,yEAAyE;AACzE,eAAO,MAAM,gBAAgB,aAAyC,CAAA;AAEtE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iCAAiC,CAC/C,CAAC,SAAS;IAAE,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,EAC3C,UAAU,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,CAyCtB;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,8EAA8E;IAC9E,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/proxy/messages.ts"],"names":[],"mappings":"AAAA;;GAEG;AAcH;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,CAiBrD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAUtE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAM7D;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,CAKzH;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,4BAA4B,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAU3D;AAED,yEAAyE;AACzE,eAAO,MAAM,gBAAgB,aAAyC,CAAA;AAEtE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iCAAiC,CAC/C,CAAC,SAAS;IAAE,OAAO,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,EAC3C,UAAU,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,CAyCtB;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,8EAA8E;IAC9E,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B;;;;;;;OAOG;IACH,cAAc,EAAE,MAAM,GAAG,SAAS,CAAA;CACnC;AAyCD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,CAAC,GAC9C,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAyB3B;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAO3D"}
|
package/dist/proxy/profiles.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* This is a leaf module — no imports from server.ts or session/.
|
|
15
15
|
*/
|
|
16
|
+
import { type RoutingMode } from "./routing";
|
|
16
17
|
/**
|
|
17
18
|
* Load profiles from ~/.config/meridian/profiles.json.
|
|
18
19
|
* Cached with a 5s TTL so new profiles are picked up without restart,
|
|
@@ -70,14 +71,28 @@ export declare function enableDiskProfileDiscovery(): void;
|
|
|
70
71
|
export declare function getEffectiveProfiles(configProfiles: ProfileConfig[] | undefined): ProfileConfig[];
|
|
71
72
|
/** Check if any profiles are available from any source */
|
|
72
73
|
export declare function hasProfiles(configProfiles: ProfileConfig[] | undefined): boolean;
|
|
74
|
+
/** Options for the sticky-routing resolution step (#383). */
|
|
75
|
+
export interface ResolveProfileOptions {
|
|
76
|
+
/** Session identity for sticky assignment (adapter.getSessionId). */
|
|
77
|
+
stickySessionKey?: string;
|
|
78
|
+
/** Routing mode — "active" (default, pre-#383 chain) or "sticky". */
|
|
79
|
+
routingMode?: RoutingMode;
|
|
80
|
+
}
|
|
73
81
|
/**
|
|
74
82
|
* Resolve a profile from the configuration.
|
|
75
83
|
*
|
|
84
|
+
* Priority: header > sticky assignment (routing="sticky" only) > active >
|
|
85
|
+
* config default > first profile. The sticky step exists so multi-account
|
|
86
|
+
* setups can distribute sessions across profiles WITHOUT losing per-account
|
|
87
|
+
* prompt caching — see routing.ts. With routingMode unset/"active" the
|
|
88
|
+
* chain is exactly the pre-#383 behavior.
|
|
89
|
+
*
|
|
76
90
|
* @param profiles - Configured profiles (from ProxyConfig)
|
|
77
91
|
* @param defaultProfile - Default profile ID (from ProxyConfig)
|
|
78
92
|
* @param requestedId - Explicit profile ID from request header
|
|
93
|
+
* @param options - Sticky-routing inputs (session key + mode)
|
|
79
94
|
*/
|
|
80
|
-
export declare function resolveProfile(profiles: ProfileConfig[] | undefined, defaultProfile: string | undefined, requestedId?: string): ResolvedProfile;
|
|
95
|
+
export declare function resolveProfile(profiles: ProfileConfig[] | undefined, defaultProfile: string | undefined, requestedId?: string, options?: ResolveProfileOptions): ResolvedProfile;
|
|
81
96
|
/**
|
|
82
97
|
* Get all configured profile IDs with their types.
|
|
83
98
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../../src/proxy/profiles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;
|
|
1
|
+
{"version":3,"file":"profiles.d.ts","sourceRoot":"","sources":["../../src/proxy/profiles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAS/D;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,aAAa,EAAE,CAkBtD;AAED,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,KAAK,GAAG,aAAa,CAAA;AAE9D,MAAM,WAAW,aAAa;IAC5B,0DAA0D;IAC1D,EAAE,EAAE,MAAM,CAAA;IACV;;;;;OAKG;IACH,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,6DAA6D;IAC7D,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,WAAW,CAAA;IACjB,4DAA4D;IAC5D,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC5B;AAOD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAGxD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,SAAS,CAEvD;AAED,+CAA+C;AAC/C,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,cAAc,CAAC,EAAE,aAAa,EAAE,GAAG,IAAI,CAY3E;AAUD;;kEAEkE;AAClE,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD;AAED,wBAAgB,oBAAoB,CAAC,cAAc,EAAE,aAAa,EAAE,GAAG,SAAS,GAAG,aAAa,EAAE,CAOjG;AAED,0DAA0D;AAC1D,wBAAgB,WAAW,CAAC,cAAc,EAAE,aAAa,EAAE,GAAG,SAAS,GAAG,OAAO,CAEhF;AAED,6DAA6D;AAC7D,MAAM,WAAW,qBAAqB;IACpC,qEAAqE;IACrE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,qEAAqE;IACrE,WAAW,CAAC,EAAE,WAAW,CAAA;CAC1B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,aAAa,EAAE,GAAG,SAAS,EACrC,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,WAAW,CAAC,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,eAAe,CAyBjB;AAkCD;;GAEG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,aAAa,EAAE,GAAG,SAAS,EACrC,cAAc,EAAE,MAAM,GAAG,SAAS,GACjC,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,CAU7D"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sticky session-to-profile routing (#383, design by @ShreeMulay).
|
|
3
|
+
*
|
|
4
|
+
* With multiple profiles configured, `routing = "sticky"` distributes
|
|
5
|
+
* sessions across profiles while preserving session affinity — Anthropic's
|
|
6
|
+
* prompt caching is per-account, so a session that flip-flops between
|
|
7
|
+
* accounts pays a cold cache (full context re-creation) on every flip.
|
|
8
|
+
*
|
|
9
|
+
* Assignment uses rendezvous (highest-random-weight) hashing:
|
|
10
|
+
* - deterministic and stateless — stickiness survives proxy restarts with
|
|
11
|
+
* no persisted session→profile map to lose or corrupt
|
|
12
|
+
* - minimal disruption — adding an arm only moves the sessions that hash
|
|
13
|
+
* to the new arm; removing an arm only reassigns that arm's sessions
|
|
14
|
+
*
|
|
15
|
+
* Resolution priority (see resolveProfile): explicit x-meridian-profile
|
|
16
|
+
* header > sticky assignment > active profile > config default > first.
|
|
17
|
+
* Default mode is "active" (the pre-#383 chain) — existing setups are
|
|
18
|
+
* byte-identical unless routing is explicitly enabled.
|
|
19
|
+
*
|
|
20
|
+
* This is a leaf module — pure functions, no I/O.
|
|
21
|
+
*/
|
|
22
|
+
export type RoutingMode = "active" | "sticky";
|
|
23
|
+
/**
|
|
24
|
+
* Parse a routing mode string (from settings or MERIDIAN_ROUTING).
|
|
25
|
+
* Unknown values fall back to "active" — a typo must never change
|
|
26
|
+
* routing behavior into something surprising.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getRoutingMode(raw: string | undefined): RoutingMode;
|
|
29
|
+
/**
|
|
30
|
+
* Pick the sticky profile for a session: the profile with the highest
|
|
31
|
+
* rendezvous score. Returns undefined when there is nothing to pick
|
|
32
|
+
* (no session identity or no profiles) — callers fall through to the
|
|
33
|
+
* normal resolution chain.
|
|
34
|
+
*/
|
|
35
|
+
export declare function pickStickyProfile(sessionKey: string, profileIds: readonly string[]): string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Pinned (sessionKey, profiles, expected) triples guarding hash stability.
|
|
38
|
+
* If an implementation change breaks these, every user's sessions would
|
|
39
|
+
* silently reshuffle onto different accounts (cold caches) on upgrade —
|
|
40
|
+
* treat that as a breaking change requiring a migration note, not a test
|
|
41
|
+
* to update casually.
|
|
42
|
+
*/
|
|
43
|
+
export declare const RENDEZVOUS_STABLE_GUARD: ReadonlyArray<readonly [string, readonly string[], string]>;
|
|
44
|
+
//# sourceMappingURL=routing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/proxy/routing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAE7C;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,CAEnE;AAaD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAYvG;AAED;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,CAAC,CAM/F,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAGvD,YAAY,EACV,SAAS,EACT,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,WAAW,GACZ,MAAM,aAAa,CAAA;AAKpB,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/proxy/server.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,CAAA;AAGvD,YAAY,EACV,SAAS,EACT,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,WAAW,GACZ,MAAM,aAAa,CAAA;AAKpB,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAsCnG,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EAEpB,KAAK,aAAa,EAGnB,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAA+B,iBAAiB,EAAE,mBAAmB,EAAsC,MAAM,iBAAiB,CAAA;AAGzI,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,CAAA;AACjD,YAAY,EAAE,aAAa,EAAE,CAAA;AA+Q7B,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CA6rGhF;AAWD,wBAAgB,gCAAgC,IAAI,IAAI,CAavD;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAmGhG"}
|
package/dist/proxy/settings.d.ts
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
export interface MeridianSettings {
|
|
11
11
|
/** Last active profile ID — restored on proxy startup */
|
|
12
12
|
activeProfile?: string;
|
|
13
|
+
/** Profile routing mode (#383): "active" (default) or "sticky".
|
|
14
|
+
* MERIDIAN_ROUTING env var takes precedence when set. */
|
|
15
|
+
routing?: string;
|
|
13
16
|
}
|
|
14
17
|
/** Read settings from disk. Returns empty object if file doesn't exist or is invalid. */
|
|
15
18
|
export declare function loadSettings(): MeridianSettings;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/proxy/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAQH,MAAM,WAAW,gBAAgB;IAC/B,yDAAyD;IACzD,aAAa,CAAC,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/proxy/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAQH,MAAM,WAAW,gBAAgB;IAC/B,yDAAyD;IACzD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;8DAC0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,yFAAyF;AACzF,wBAAgB,YAAY,IAAI,gBAAgB,CAO/C;AAED,4FAA4F;AAC5F,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,IAAI,CASrE;AAED,iCAAiC;AACjC,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAAE,GAAG,EAAE,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAExF;AAED,6CAA6C;AAC7C,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,IAAI,CAErG"}
|
package/dist/server.js
CHANGED
|
@@ -11,8 +11,8 @@ import {
|
|
|
11
11
|
runObserveHook,
|
|
12
12
|
runTransformHook,
|
|
13
13
|
startProxyServer
|
|
14
|
-
} from "./cli-
|
|
15
|
-
import"./cli-
|
|
14
|
+
} from "./cli-red47yv3.js";
|
|
15
|
+
import"./cli-f0yqy2d2.js";
|
|
16
16
|
import"./cli-sry5aqdj.js";
|
|
17
17
|
import"./cli-4rqtm83g.js";
|
|
18
18
|
import"./cli-q6dz9r5p.js";
|
package/package.json
CHANGED