@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 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 resolvedId = requestedId || activeProfileId || defaultProfile || effective[0].id;
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-cx463q74.js";
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 existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync as renameSync2 } from "node:fs";
1202
- import { join as join4 } from "node:path";
1203
- import { homedir as homedir3 } from "node:os";
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 = join4(homedir3(), ".config", "meridian");
1206
- if (!existsSync5(dir))
1209
+ const dir = join5(homedir4(), ".config", "meridian");
1210
+ if (!existsSync6(dir))
1207
1211
  mkdirSync2(dir, { recursive: true });
1208
- return join4(dir, "sdk-features.json");
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 (existsSync5(path3)) {
1217
- cachedConfig = JSON.parse(readFileSync4(path3, "utf-8"));
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 homedir4 } from "node:os";
3742
- import { join as join5 } from "node:path";
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, { name: block.name, target });
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
- return info.target ? `[${info.name} ${info.target}]` : `[${info.name}]`;
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 readFileSync2, existsSync as existsSync3 } from "fs";
17186
- import { join as join2, isAbsolute as isAbsolute2, extname } from "path";
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 (!existsSync3(configPath))
17379
+ if (!existsSync4(configPath))
17228
17380
  return [];
17229
17381
  try {
17230
- const raw2 = readFileSync2(configPath, "utf-8");
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 = existsSync3(pluginDir);
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 : join2(pluginDir, 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 existsSync4,
17796
+ existsSync as existsSync5,
17645
17797
  mkdirSync,
17646
17798
  openSync,
17647
- readFileSync as readFileSync3,
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 homedir2 } from "node:os";
17654
- import { join as join3 } from "node:path";
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 (!existsSync4(dir)) {
17854
+ if (!existsSync5(dir)) {
17703
17855
  mkdirSync(dir, { recursive: true });
17704
17856
  }
17705
- return join3(dir, "sessions.json");
17857
+ return join4(dir, "sessions.json");
17706
17858
  }
17707
17859
  function getDefaultCacheDir() {
17708
- const newDir = join3(homedir2(), ".cache", "meridian");
17709
- const oldDir = join3(homedir2(), ".cache", "opencode-claude-max-proxy");
17710
- if (existsSync4(newDir))
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 (existsSync4(oldDir)) {
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 (!existsSync4(path3))
17877
+ if (!existsSync5(path3))
17726
17878
  return {};
17727
17879
  try {
17728
- const data = readFileSync3(path3, "utf-8");
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
- const role = m.role === "assistant" ? "Assistant" : "Human";
18192
- const content = m.role === "assistant" ? flattenAssistantContent(m.content) : flattenUserContent(m.content, sanitizeOpts, toolIndex);
18193
- return content ? `${role}: ${content}` : "";
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 ?? join5(homedir4(), ".config", "meridian", "plugins");
18258
- const pluginConfigPath = finalConfig.pluginConfigPath ?? join5(homedir4(), ".config", "meridian", "plugins.json");
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 profile = resolveProfile(finalConfig.profiles, finalConfig.defaultProfile, c.req.header("x-meridian-profile") || undefined);
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 adapterTransforms = getAdapterTransforms(adapter.name);
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: adapter.name,
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
- }), adapter.name);
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.name);
18413
- if (getExplicitThinking2(adapter.name) === "disabled") {
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 = adapter.name !== "claude-code" && !agentSessionId && lastIsToolResult;
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" && adapter.name === "opencode" && !agentSessionId) {
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
- const role = m.role === "assistant" ? "Assistant" : "Human";
18545
- const content = m.role === "assistant" ? flattenAssistantContent(m.content) : flattenUserContent(m.content, sanitizeOpts, toolIndex);
18546
- return content ? `${role}: ${content}` : "";
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-da9hc1s1.js";
5
- import"./cli-cx463q74.js";
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-rdd84b45.js");
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 });
@@ -9,7 +9,7 @@ import {
9
9
  resolveProfile,
10
10
  restoreActiveProfile,
11
11
  setActiveProfile
12
- } from "./cli-cx463q74.js";
12
+ } from "./cli-f0yqy2d2.js";
13
13
  import"./cli-340h1chz.js";
14
14
  import"./cli-p9swy5t3.js";
15
15
  export {
@@ -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;AAkD9C;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,YAAY,CAgDtD"}
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"}
@@ -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]` or `[bash echo hi]`. Deliberately terse and
102
- * bracket-formatted differently from the banned `[Tool Use: ...]` /
103
- * `[Tool Result ...]` shapes (#111/#386) so the model reads it as context,
104
- * not as tool-call syntax to imitate.
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;CAC3B;AAKD;;;;;;;;;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,CAqB3B;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAE3D"}
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"}
@@ -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;AAcH;;;;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;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,aAAa,EAAE,GAAG,SAAS,EACrC,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,WAAW,CAAC,EAAE,MAAM,GACnB,eAAe,CAkBjB;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"}
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;AAoCnG,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;AA2Q7B,wBAAgB,iBAAiB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CA+pGhF;AAWD,wBAAgB,gCAAgC,IAAI,IAAI,CAavD;AAED,wBAAsB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAmGhG"}
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"}
@@ -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;CACvB;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"}
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-da9hc1s1.js";
15
- import"./cli-cx463q74.js";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynfar/meridian",
3
- "version": "1.48.0",
3
+ "version": "1.49.0",
4
4
  "description": "Local Anthropic API powered by your Claude Max subscription. One subscription, every agent.",
5
5
  "type": "module",
6
6
  "main": "./dist/server.js",