clawmux 0.3.13 → 0.3.15

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
@@ -83,6 +83,8 @@ ClawMux registers as a single provider `clawmux` in OpenClaw with model `auto`.
83
83
  openclaw provider clawmux
84
84
  ```
85
85
 
86
+ `clawmux init` manages the `clawmux` provider entry in `openclaw.json` (and the per-agent `models.json` caches) for you. It sets `api` to match your MEDIUM tier's API format and computes the correct `baseUrl` from that — `http://localhost:<port>/v1` for OpenAI-style APIs (where the upstream SDK appends `/chat/completions` or `/responses`) and `http://localhost:<port>` for everything else. Do not hand-edit these fields; rerun `clawmux init` after changing the MEDIUM model in `~/.openclaw/clawmux.json`.
87
+
86
88
  ## How It Works
87
89
 
88
90
  ```
@@ -109,7 +111,7 @@ ClawMux resolves each model's context window using this priority chain:
109
111
 
110
112
  1. **~/.openclaw/clawmux.json** `routing.contextWindows` — explicit per-model override
111
113
  2. **openclaw.json** `models.providers[provider].models[].contextWindow` — user config
112
- 3. **OpenClaw built-in catalog** — pi-ai model database (812+ models)
114
+ 3. **OpenClaw built-in catalog** — pi-ai model database (830+ models)
113
115
  4. **Default: 200,000 tokens**
114
116
 
115
117
  Compression threshold uses the **minimum** context window across all routing models, since compression happens before routing decides which model to use.
package/dist/cli.cjs CHANGED
@@ -3,6 +3,7 @@ var __create = Object.create;
3
3
  var __getProtoOf = Object.getPrototypeOf;
4
4
  var __defProp = Object.defineProperty;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
8
  function __accessProp(key) {
8
9
  return this[key];
@@ -29,6 +30,23 @@ var __toESM = (mod, isNodeMode, target) => {
29
30
  cache.set(mod, to);
30
31
  return to;
31
32
  };
33
+ var __toCommonJS = (from) => {
34
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
35
+ if (entry)
36
+ return entry;
37
+ entry = __defProp({}, "__esModule", { value: true });
38
+ if (from && typeof from === "object" || typeof from === "function") {
39
+ for (var key of __getOwnPropNames(from))
40
+ if (!__hasOwnProp.call(entry, key))
41
+ __defProp(entry, key, {
42
+ get: __accessProp.bind(from, key),
43
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
44
+ });
45
+ }
46
+ __moduleCache.set(from, entry);
47
+ return entry;
48
+ };
49
+ var __moduleCache;
32
50
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
33
51
  var __returnValue = (v) => v;
34
52
  function __exportSetter(name, newValue) {
@@ -112214,13 +112232,18 @@ var require_dist4 = __commonJS((exports2, module2) => {
112214
112232
  });
112215
112233
 
112216
112234
  // src/cli.ts
112235
+ var exports_cli = {};
112236
+ __export(exports_cli, {
112237
+ resolveProviderBaseUrl: () => resolveProviderBaseUrl
112238
+ });
112239
+ module.exports = __toCommonJS(exports_cli);
112217
112240
  var import_promises8 = require("node:fs/promises");
112218
112241
  var import_node_path7 = require("node:path");
112219
112242
  var import_node_child_process = require("node:child_process");
112220
112243
  var import_node_os2 = require("node:os");
112221
112244
 
112222
112245
  // src/proxy/router.ts
112223
- var VERSION = process.env.npm_package_version ?? "0.3.13";
112246
+ var VERSION = process.env.npm_package_version ?? "0.3.15";
112224
112247
  function jsonResponse(body, status = 200) {
112225
112248
  return new Response(JSON.stringify(body), {
112226
112249
  status,
@@ -115091,6 +115114,9 @@ function djb2Hash(text) {
115091
115114
  return (hash >>> 0).toString(16);
115092
115115
  }
115093
115116
  function extractText(content) {
115117
+ if (content === null || content === undefined) {
115118
+ return "";
115119
+ }
115094
115120
  if (typeof content === "string") {
115095
115121
  return content.slice(0, 200);
115096
115122
  }
@@ -115103,6 +115129,9 @@ function extractText(content) {
115103
115129
  return concatenated.slice(0, 200);
115104
115130
  }
115105
115131
  function contentLength(content) {
115132
+ if (content === null || content === undefined) {
115133
+ return 0;
115134
+ }
115106
115135
  if (typeof content === "string") {
115107
115136
  return content.length;
115108
115137
  }
@@ -115178,7 +115207,7 @@ class EscalationMemory {
115178
115207
  }
115179
115208
 
115180
115209
  // src/routing/instruction-injector.ts
115181
- var INJECT_FOR_TIERS = new Set(["LIGHT"]);
115210
+ var INJECT_FOR_TIERS = new Set(["LIGHT", "MEDIUM"]);
115182
115211
  var ESCALATION_INSTRUCTION = `If you cannot handle this request fully (due to complexity, missing context, or capability limits), output EXACTLY the following marker with no other text on that line: ${ESCALATE_SIGNAL}
115183
115212
  Do not explain. Do not ask permission. Just emit the marker and stop.`;
115184
115213
  function injectEscalationInstruction(messages) {
@@ -115194,6 +115223,12 @@ function injectEscalationInstruction(messages) {
115194
115223
  ...rest
115195
115224
  ];
115196
115225
  }
115226
+ if (first.content === null) {
115227
+ return [
115228
+ { role: "system", content: ESCALATION_INSTRUCTION },
115229
+ ...rest
115230
+ ];
115231
+ }
115197
115232
  if (typeof first.content === "string") {
115198
115233
  return [
115199
115234
  { role: "system", content: first.content + `
@@ -119730,8 +119765,7 @@ function piStreamToAnthropicSse(piStream) {
119730
119765
  }
119731
119766
  });
119732
119767
  }
119733
- async function piStreamToAnthropicJson(piStream) {
119734
- const msg = await piStream.result();
119768
+ function anthropicMessageFromAssistant(msg) {
119735
119769
  if ((msg.stopReason === "error" || msg.stopReason === "aborted") && msg.errorMessage) {
119736
119770
  console.error(`[clawmux] pi-ai upstream error (${msg.stopReason}): ${msg.errorMessage}`);
119737
119771
  }
@@ -119769,6 +119803,10 @@ async function piStreamToAnthropicJson(piStream) {
119769
119803
  ...msg.errorMessage ? { error_message: msg.errorMessage } : {}
119770
119804
  };
119771
119805
  }
119806
+ async function piStreamToAnthropicJson(piStream) {
119807
+ const msg = await piStream.result();
119808
+ return anthropicMessageFromAssistant(msg);
119809
+ }
119772
119810
 
119773
119811
  // src/pi-bridge/event-to-openai-completions.ts
119774
119812
  var encoder3 = new TextEncoder;
@@ -119992,8 +120030,7 @@ function piStreamToOpenAiCompletionsSse(piStream) {
119992
120030
  }
119993
120031
  });
119994
120032
  }
119995
- async function piStreamToOpenAiCompletionsJson(piStream) {
119996
- const msg = await piStream.result();
120033
+ function openAiCompletionsMessageFromAssistant(msg) {
119997
120034
  let textContent = "";
119998
120035
  const toolCalls = [];
119999
120036
  let reasoning = "";
@@ -120043,6 +120080,10 @@ async function piStreamToOpenAiCompletionsJson(piStream) {
120043
120080
  }
120044
120081
  };
120045
120082
  }
120083
+ async function piStreamToOpenAiCompletionsJson(piStream) {
120084
+ const msg = await piStream.result();
120085
+ return openAiCompletionsMessageFromAssistant(msg);
120086
+ }
120046
120087
 
120047
120088
  // src/pi-bridge/event-to-openai-responses.ts
120048
120089
  var encoder4 = new TextEncoder;
@@ -120205,8 +120246,7 @@ function piStreamToOpenAiResponsesSse(piStream) {
120205
120246
  }
120206
120247
  });
120207
120248
  }
120208
- async function piStreamToOpenAiResponsesJson(piStream) {
120209
- const msg = await piStream.result();
120249
+ function openAiResponsesMessageFromAssistant(msg) {
120210
120250
  const output = [];
120211
120251
  for (const c of msg.content) {
120212
120252
  if (c.type === "text") {
@@ -120247,6 +120287,10 @@ async function piStreamToOpenAiResponsesJson(piStream) {
120247
120287
  }
120248
120288
  };
120249
120289
  }
120290
+ async function piStreamToOpenAiResponsesJson(piStream) {
120291
+ const msg = await piStream.result();
120292
+ return openAiResponsesMessageFromAssistant(msg);
120293
+ }
120250
120294
 
120251
120295
  // src/pi-bridge/event-to-google.ts
120252
120296
  var encoder5 = new TextEncoder;
@@ -120363,8 +120407,7 @@ function piStreamToGoogleSse(piStream) {
120363
120407
  }
120364
120408
  });
120365
120409
  }
120366
- async function piStreamToGoogleJson(piStream) {
120367
- const msg = await piStream.result();
120410
+ function googleMessageFromAssistant(msg) {
120368
120411
  const parts = [];
120369
120412
  for (const c of msg.content) {
120370
120413
  if (c.type === "text") {
@@ -120396,6 +120439,10 @@ async function piStreamToGoogleJson(piStream) {
120396
120439
  modelVersion: msg.model || ""
120397
120440
  };
120398
120441
  }
120442
+ async function piStreamToGoogleJson(piStream) {
120443
+ const msg = await piStream.result();
120444
+ return googleMessageFromAssistant(msg);
120445
+ }
120399
120446
 
120400
120447
  // src/proxy/pipeline.ts
120401
120448
  registerAdapter(new AnthropicAdapter);
@@ -120632,35 +120679,12 @@ async function handleApiRequest(req, body, apiType, config, openclawConfig, auth
120632
120679
  }
120633
120680
  const detector = signalRouter.createSignalDetector();
120634
120681
  const detectionState = createSignalDetectionState();
120635
- if (wantsStream) {
120636
- const signalGen = detectSignalInStream(piStreamHandle, detector, detectionState, () => {});
120637
- const sseBody = piStreamToAnthropicSseFromGenerator(signalGen);
120638
- const response = new Response(sseBody, {
120639
- status: 200,
120640
- headers: { "content-type": "text/event-stream" }
120641
- });
120642
- response.clone().text().then(() => {
120643
- if (detectionState.signalDetected) {
120644
- abortCtrl.abort();
120645
- const nextTier = signalRouter.handleEscalation(messages, currentTier);
120646
- if (nextTier !== null) {
120647
- console.log(`[clawmux] [escalation] ${currentTier} → ${nextTier} (signal detected)`);
120648
- currentTier = nextTier;
120649
- return;
120650
- }
120651
- }
120652
- signalRouter.touchActivity(messages);
120653
- if (attempt > 0) {
120654
- signalRouter.recordSuccessfulEscalation(messages, currentTier);
120655
- }
120656
- });
120657
- return response;
120658
- }
120659
- const fullText = await collectPiStreamText(piStreamHandle, detector, detectionState);
120682
+ const assistantMsg = await collectPiStreamWithSignalDetection(piStreamHandle, detector, detectionState);
120660
120683
  if (detectionState.signalDetected) {
120684
+ abortCtrl.abort();
120661
120685
  const nextTier = signalRouter.handleEscalation(messages, currentTier);
120662
120686
  if (nextTier !== null) {
120663
- console.log(`[clawmux] [escalation] ${currentTier} → ${nextTier} (signal detected in non-streaming)`);
120687
+ console.log(`[clawmux] [escalation] ${currentTier} → ${nextTier} (signal detected)`);
120664
120688
  currentTier = nextTier;
120665
120689
  continue;
120666
120690
  }
@@ -120669,7 +120693,15 @@ async function handleApiRequest(req, body, apiType, config, openclawConfig, auth
120669
120693
  if (attempt > 0) {
120670
120694
  signalRouter.recordSuccessfulEscalation(messages, currentTier);
120671
120695
  }
120672
- return new Response(JSON.stringify(fullText), {
120696
+ if (wantsStream) {
120697
+ const sseBody = buildSseForApiType(apiType, replayAssistantMessageAsEvents(assistantMsg));
120698
+ return new Response(sseBody, {
120699
+ status: 200,
120700
+ headers: { "content-type": "text/event-stream" }
120701
+ });
120702
+ }
120703
+ const jsonBody = buildJsonForApiType(apiType, assistantMsg);
120704
+ return new Response(JSON.stringify(jsonBody), {
120673
120705
  status: 200,
120674
120706
  headers: { "content-type": "application/json" }
120675
120707
  });
@@ -120915,164 +120947,63 @@ async function yieldPiAiResponse(piStreamHandle, apiType, wantsStream) {
120915
120947
  }
120916
120948
  throw new Error(`Unsupported pi-ai apiType: ${apiType}`);
120917
120949
  }
120918
- function piStreamToAnthropicSseFromGenerator(gen) {
120919
- const encoder6 = new TextEncoder;
120920
- function sseFrame2(event, data) {
120921
- return encoder6.encode(`event: ${event}
120922
- data: ${JSON.stringify(data)}
120923
-
120924
- `);
120925
- }
120926
- return new ReadableStream({
120927
- async start(controller) {
120928
- try {
120929
- let messageStarted = false;
120930
- const openBlocks = new Map;
120931
- const ensureMessageStart = (model) => {
120932
- if (messageStarted)
120933
- return;
120934
- messageStarted = true;
120935
- controller.enqueue(sseFrame2("message_start", {
120936
- type: "message_start",
120937
- message: {
120938
- id: "msg_" + Date.now().toString(36),
120939
- type: "message",
120940
- role: "assistant",
120941
- content: [],
120942
- model,
120943
- stop_reason: null,
120944
- stop_sequence: null,
120945
- usage: { input_tokens: 0, output_tokens: 0 }
120946
- }
120947
- }));
120948
- };
120949
- for await (const event of gen) {
120950
- if (event.type === "start") {
120951
- ensureMessageStart(event.partial.model || "");
120952
- } else if (event.type === "text_start") {
120953
- ensureMessageStart(event.partial.model || "");
120954
- openBlocks.set(event.contentIndex, "text");
120955
- controller.enqueue(sseFrame2("content_block_start", {
120956
- type: "content_block_start",
120957
- index: event.contentIndex,
120958
- content_block: { type: "text", text: "" }
120959
- }));
120960
- } else if (event.type === "text_delta") {
120961
- if (openBlocks.get(event.contentIndex) !== "text") {
120962
- ensureMessageStart(event.partial.model || "");
120963
- openBlocks.set(event.contentIndex, "text");
120964
- controller.enqueue(sseFrame2("content_block_start", {
120965
- type: "content_block_start",
120966
- index: event.contentIndex,
120967
- content_block: { type: "text", text: "" }
120968
- }));
120969
- }
120970
- controller.enqueue(sseFrame2("content_block_delta", {
120971
- type: "content_block_delta",
120972
- index: event.contentIndex,
120973
- delta: { type: "text_delta", text: event.delta }
120974
- }));
120975
- } else if (event.type === "text_end") {
120976
- if (openBlocks.get(event.contentIndex) === "text") {
120977
- controller.enqueue(sseFrame2("content_block_stop", {
120978
- type: "content_block_stop",
120979
- index: event.contentIndex
120980
- }));
120981
- openBlocks.delete(event.contentIndex);
120982
- }
120983
- } else if (event.type === "done") {
120984
- for (const [idx] of openBlocks) {
120985
- controller.enqueue(sseFrame2("content_block_stop", {
120986
- type: "content_block_stop",
120987
- index: idx
120988
- }));
120989
- }
120990
- openBlocks.clear();
120991
- controller.enqueue(sseFrame2("message_delta", {
120992
- type: "message_delta",
120993
- delta: { stop_reason: "end_turn", stop_sequence: null },
120994
- usage: { output_tokens: event.message.usage?.output ?? 0 }
120995
- }));
120996
- controller.enqueue(sseFrame2("message_stop", { type: "message_stop" }));
120997
- } else if (event.type === "error") {
120998
- for (const [idx] of openBlocks) {
120999
- controller.enqueue(sseFrame2("content_block_stop", {
121000
- type: "content_block_stop",
121001
- index: idx
121002
- }));
121003
- }
121004
- openBlocks.clear();
121005
- controller.enqueue(sseFrame2("error", {
121006
- type: "error",
121007
- error: {
121008
- type: "api_error",
121009
- message: event.error?.errorMessage ?? "Unknown error"
121010
- }
121011
- }));
121012
- }
121013
- }
121014
- controller.close();
121015
- } catch (err) {
121016
- const msg = err instanceof Error ? err.message : String(err);
121017
- controller.enqueue(sseFrame2("error", {
121018
- type: "error",
121019
- error: { type: "api_error", message: msg }
121020
- }));
121021
- controller.close();
120950
+ function buildSseForApiType(apiType, gen) {
120951
+ if (apiType === "anthropic-messages")
120952
+ return piStreamToAnthropicSse(gen);
120953
+ if (apiType === "openai-completions")
120954
+ return piStreamToOpenAiCompletionsSse(gen);
120955
+ if (apiType === "openai-responses")
120956
+ return piStreamToOpenAiResponsesSse(gen);
120957
+ if (apiType === "google-generative-ai")
120958
+ return piStreamToGoogleSse(gen);
120959
+ return piStreamToAnthropicSse(gen);
120960
+ }
120961
+ function buildJsonForApiType(apiType, msg) {
120962
+ if (apiType === "anthropic-messages")
120963
+ return anthropicMessageFromAssistant(msg);
120964
+ if (apiType === "openai-completions")
120965
+ return openAiCompletionsMessageFromAssistant(msg);
120966
+ if (apiType === "openai-responses")
120967
+ return openAiResponsesMessageFromAssistant(msg);
120968
+ if (apiType === "google-generative-ai")
120969
+ return googleMessageFromAssistant(msg);
120970
+ return anthropicMessageFromAssistant(msg);
120971
+ }
120972
+ async function drainSignalGenerator(gen) {
120973
+ for await (const _event of gen) {}
120974
+ }
120975
+ async function* replayAssistantMessageAsEvents(msg) {
120976
+ const partial = msg;
120977
+ yield { type: "start", partial };
120978
+ for (let i2 = 0;i2 < msg.content.length; i2++) {
120979
+ const block = msg.content[i2];
120980
+ if (block.type === "text") {
120981
+ yield { type: "text_start", contentIndex: i2, partial };
120982
+ if (block.text.length > 0) {
120983
+ yield { type: "text_delta", contentIndex: i2, delta: block.text, partial };
121022
120984
  }
121023
- }
121024
- });
121025
- }
121026
- async function collectPiStreamText(piStreamHandle, detector, state) {
121027
- const msg = await piStreamHandle.result();
121028
- const fullText = msg.content.filter((c) => c.type === "text").map((c) => c.text).join("");
121029
- const results = detector.feedChunk(fullText);
121030
- for (const r2 of results) {
121031
- if (r2.type === "signal_detected") {
121032
- state.signalDetected = true;
121033
- break;
121034
- }
121035
- }
121036
- detector.flush();
121037
- const STOP_REASON_MAP2 = {
121038
- stop: "end_turn",
121039
- length: "max_tokens",
121040
- toolUse: "tool_use",
121041
- error: "end_turn",
121042
- aborted: "end_turn"
121043
- };
121044
- const blocks = [];
121045
- for (const c of msg.content) {
121046
- if (c.type === "text") {
121047
- blocks.push({ type: "text", text: c.text });
121048
- } else if (c.type === "thinking") {
121049
- blocks.push({
121050
- type: "thinking",
121051
- thinking: c.thinking,
121052
- ...c.thinkingSignature ? { signature: c.thinkingSignature } : {}
121053
- });
121054
- } else if (c.type === "toolCall") {
121055
- blocks.push({
121056
- type: "tool_use",
121057
- id: c.id,
121058
- name: c.name,
121059
- input: c.arguments ?? {}
121060
- });
120985
+ yield { type: "text_end", contentIndex: i2, content: block.text, partial };
120986
+ } else if (block.type === "thinking") {
120987
+ yield { type: "thinking_start", contentIndex: i2, partial };
120988
+ if (block.thinking.length > 0) {
120989
+ yield { type: "thinking_delta", contentIndex: i2, delta: block.thinking, partial };
120990
+ }
120991
+ yield { type: "thinking_end", contentIndex: i2, content: block.thinking, partial };
120992
+ } else if (block.type === "toolCall") {
120993
+ yield { type: "toolcall_start", contentIndex: i2, partial };
120994
+ const argsJson = JSON.stringify(block.arguments ?? {});
120995
+ if (argsJson.length > 0 && argsJson !== "{}") {
120996
+ yield { type: "toolcall_delta", contentIndex: i2, delta: argsJson, partial };
120997
+ }
120998
+ yield { type: "toolcall_end", contentIndex: i2, toolCall: block, partial };
121061
120999
  }
121062
121000
  }
121063
- return {
121064
- id: "msg_" + Date.now().toString(36),
121065
- type: "message",
121066
- role: "assistant",
121067
- content: blocks,
121068
- model: msg.model || "",
121069
- stop_reason: STOP_REASON_MAP2[msg.stopReason] ?? "end_turn",
121070
- stop_sequence: null,
121071
- usage: {
121072
- input_tokens: msg.usage?.input ?? 0,
121073
- output_tokens: msg.usage?.output ?? 0
121074
- }
121075
- };
121001
+ yield { type: "done", reason: msg.stopReason, message: msg };
121002
+ }
121003
+ async function collectPiStreamWithSignalDetection(piStreamHandle, detector, state) {
121004
+ const signalGen = detectSignalInStream(piStreamHandle, detector, state, () => {});
121005
+ await drainSignalGenerator(signalGen);
121006
+ return await piStreamHandle.result();
121076
121007
  }
121077
121008
  var ROUTE_MAPPINGS = [
121078
121009
  { apiType: "anthropic-messages", key: "/v1/messages" },
@@ -121235,6 +121166,13 @@ function resolveProviderApi(mediumModel, openclawProviders) {
121235
121166
  return api;
121236
121167
  return PROVIDER_API_FALLBACK;
121237
121168
  }
121169
+ function resolveProviderBaseUrl(apiType, port) {
121170
+ const origin = `http://localhost:${port}`;
121171
+ if (apiType === "openai-completions" || apiType === "openai-responses") {
121172
+ return `${origin}/v1`;
121173
+ }
121174
+ return origin;
121175
+ }
121238
121176
  async function fileExistsLocal(path4) {
121239
121177
  try {
121240
121178
  await import_promises8.access(path4);
@@ -121513,7 +121451,7 @@ async function update() {
121513
121451
  process.exit(1);
121514
121452
  }
121515
121453
  }
121516
- async function fixAgentModelsJson(homeDir, providerKey, correctBaseUrl) {
121454
+ async function fixAgentModelsJson(homeDir, providerKey, servicePort, fallbackApiType) {
121517
121455
  const agentsDir = import_node_path7.join(homeDir, ".openclaw", "agents");
121518
121456
  let agentDirs;
121519
121457
  try {
@@ -121531,13 +121469,15 @@ async function fixAgentModelsJson(homeDir, providerKey, correctBaseUrl) {
121531
121469
  if (!agentProviders?.[providerKey])
121532
121470
  continue;
121533
121471
  const entry = agentProviders[providerKey];
121472
+ const agentApiType = typeof entry["api"] === "string" && entry["api"].length > 0 ? entry["api"] : fallbackApiType;
121473
+ const correctBaseUrl = resolveProviderBaseUrl(agentApiType, servicePort);
121534
121474
  const current = String(entry["baseUrl"] ?? "");
121535
121475
  if (current === correctBaseUrl)
121536
121476
  continue;
121537
121477
  entry["baseUrl"] = correctBaseUrl;
121538
121478
  await import_promises8.writeFile(modelsPath, JSON.stringify(data, null, 2) + `
121539
121479
  `);
121540
- console.log(` fixed agent ${agentId} ${providerKey} baseUrl: ${current} → ${correctBaseUrl}`);
121480
+ console.log(` fixed agent ${agentId} ${providerKey} baseUrl: ${current} → ${correctBaseUrl} (api=${agentApiType})`);
121541
121481
  } catch {}
121542
121482
  }
121543
121483
  }
@@ -121596,31 +121536,42 @@ async function init() {
121596
121536
  } catch {
121597
121537
  console.log(`[info] clawmux.json not readable, using default api: ${providerApi}`);
121598
121538
  }
121539
+ const servicePort = process.env.CLAWMUX_PORT ?? "3456";
121540
+ const providerBaseUrl = resolveProviderBaseUrl(providerApi, servicePort);
121599
121541
  if (providers[PROVIDER_KEY]) {
121600
121542
  const existing = providers[PROVIDER_KEY];
121601
- if (existing["api"] !== providerApi) {
121543
+ const existingApi = existing["api"];
121544
+ const existingBaseUrl = existing["baseUrl"];
121545
+ let dirty = false;
121546
+ if (existingApi !== providerApi) {
121602
121547
  existing["api"] = providerApi;
121548
+ dirty = true;
121549
+ }
121550
+ if (existingBaseUrl !== providerBaseUrl) {
121551
+ existing["baseUrl"] = providerBaseUrl;
121552
+ dirty = true;
121553
+ }
121554
+ if (dirty) {
121603
121555
  await import_promises8.writeFile(openclawConfigPath, JSON.stringify(config, null, 2) + `
121604
121556
  `);
121605
- console.log(` updated ${PROVIDER_KEY} provider api → ${providerApi}`);
121557
+ console.log(` updated ${PROVIDER_KEY} provider (api=${providerApi}, baseUrl=${providerBaseUrl})`);
121606
121558
  } else {
121607
- console.log(` skip ${PROVIDER_KEY} (already exists, api=${providerApi})`);
121559
+ console.log(` skip ${PROVIDER_KEY} (already exists, api=${providerApi}, baseUrl=${providerBaseUrl})`);
121608
121560
  }
121609
121561
  } else {
121610
121562
  providers[PROVIDER_KEY] = {
121611
- baseUrl: "http://localhost:3456",
121563
+ baseUrl: providerBaseUrl,
121612
121564
  api: providerApi,
121613
121565
  models: [{ id: "auto", name: "ClawMux Auto Router" }]
121614
121566
  };
121615
121567
  await import_promises8.writeFile(openclawConfigPath, JSON.stringify(config, null, 2) + `
121616
121568
  `);
121617
- console.log(` added ${PROVIDER_KEY} provider to openclaw.json (api=${providerApi})`);
121569
+ console.log(` added ${PROVIDER_KEY} provider to openclaw.json (api=${providerApi}, baseUrl=${providerBaseUrl})`);
121618
121570
  }
121619
- await fixAgentModelsJson(homeDir, PROVIDER_KEY, "http://localhost:3456");
121620
- const port = process.env.CLAWMUX_PORT ?? "3456";
121571
+ await fixAgentModelsJson(homeDir, PROVIDER_KEY, servicePort, providerApi);
121621
121572
  if (!noService) {
121622
121573
  console.log("");
121623
- await installService(port, process.cwd());
121574
+ await installService(servicePort, process.cwd());
121624
121575
  }
121625
121576
  console.log(`
121626
121577
  [info] ClawMux setup complete!`);
package/dist/index.cjs CHANGED
@@ -112238,7 +112238,7 @@ __export(exports_src2, {
112238
112238
  module.exports = __toCommonJS(exports_src2);
112239
112239
 
112240
112240
  // src/proxy/router.ts
112241
- var VERSION = process.env.npm_package_version ?? "0.3.13";
112241
+ var VERSION = process.env.npm_package_version ?? "0.3.15";
112242
112242
  function jsonResponse(body, status = 200) {
112243
112243
  return new Response(JSON.stringify(body), {
112244
112244
  status,
@@ -115109,6 +115109,9 @@ function djb2Hash(text) {
115109
115109
  return (hash >>> 0).toString(16);
115110
115110
  }
115111
115111
  function extractText(content) {
115112
+ if (content === null || content === undefined) {
115113
+ return "";
115114
+ }
115112
115115
  if (typeof content === "string") {
115113
115116
  return content.slice(0, 200);
115114
115117
  }
@@ -115121,6 +115124,9 @@ function extractText(content) {
115121
115124
  return concatenated.slice(0, 200);
115122
115125
  }
115123
115126
  function contentLength(content) {
115127
+ if (content === null || content === undefined) {
115128
+ return 0;
115129
+ }
115124
115130
  if (typeof content === "string") {
115125
115131
  return content.length;
115126
115132
  }
@@ -115196,7 +115202,7 @@ class EscalationMemory {
115196
115202
  }
115197
115203
 
115198
115204
  // src/routing/instruction-injector.ts
115199
- var INJECT_FOR_TIERS = new Set(["LIGHT"]);
115205
+ var INJECT_FOR_TIERS = new Set(["LIGHT", "MEDIUM"]);
115200
115206
  var ESCALATION_INSTRUCTION = `If you cannot handle this request fully (due to complexity, missing context, or capability limits), output EXACTLY the following marker with no other text on that line: ${ESCALATE_SIGNAL}
115201
115207
  Do not explain. Do not ask permission. Just emit the marker and stop.`;
115202
115208
  function injectEscalationInstruction(messages) {
@@ -115212,6 +115218,12 @@ function injectEscalationInstruction(messages) {
115212
115218
  ...rest
115213
115219
  ];
115214
115220
  }
115221
+ if (first.content === null) {
115222
+ return [
115223
+ { role: "system", content: ESCALATION_INSTRUCTION },
115224
+ ...rest
115225
+ ];
115226
+ }
115215
115227
  if (typeof first.content === "string") {
115216
115228
  return [
115217
115229
  { role: "system", content: first.content + `
@@ -119748,8 +119760,7 @@ function piStreamToAnthropicSse(piStream) {
119748
119760
  }
119749
119761
  });
119750
119762
  }
119751
- async function piStreamToAnthropicJson(piStream) {
119752
- const msg = await piStream.result();
119763
+ function anthropicMessageFromAssistant(msg) {
119753
119764
  if ((msg.stopReason === "error" || msg.stopReason === "aborted") && msg.errorMessage) {
119754
119765
  console.error(`[clawmux] pi-ai upstream error (${msg.stopReason}): ${msg.errorMessage}`);
119755
119766
  }
@@ -119787,6 +119798,10 @@ async function piStreamToAnthropicJson(piStream) {
119787
119798
  ...msg.errorMessage ? { error_message: msg.errorMessage } : {}
119788
119799
  };
119789
119800
  }
119801
+ async function piStreamToAnthropicJson(piStream) {
119802
+ const msg = await piStream.result();
119803
+ return anthropicMessageFromAssistant(msg);
119804
+ }
119790
119805
 
119791
119806
  // src/pi-bridge/event-to-openai-completions.ts
119792
119807
  var encoder3 = new TextEncoder;
@@ -120010,8 +120025,7 @@ function piStreamToOpenAiCompletionsSse(piStream) {
120010
120025
  }
120011
120026
  });
120012
120027
  }
120013
- async function piStreamToOpenAiCompletionsJson(piStream) {
120014
- const msg = await piStream.result();
120028
+ function openAiCompletionsMessageFromAssistant(msg) {
120015
120029
  let textContent = "";
120016
120030
  const toolCalls = [];
120017
120031
  let reasoning = "";
@@ -120061,6 +120075,10 @@ async function piStreamToOpenAiCompletionsJson(piStream) {
120061
120075
  }
120062
120076
  };
120063
120077
  }
120078
+ async function piStreamToOpenAiCompletionsJson(piStream) {
120079
+ const msg = await piStream.result();
120080
+ return openAiCompletionsMessageFromAssistant(msg);
120081
+ }
120064
120082
 
120065
120083
  // src/pi-bridge/event-to-openai-responses.ts
120066
120084
  var encoder4 = new TextEncoder;
@@ -120223,8 +120241,7 @@ function piStreamToOpenAiResponsesSse(piStream) {
120223
120241
  }
120224
120242
  });
120225
120243
  }
120226
- async function piStreamToOpenAiResponsesJson(piStream) {
120227
- const msg = await piStream.result();
120244
+ function openAiResponsesMessageFromAssistant(msg) {
120228
120245
  const output = [];
120229
120246
  for (const c of msg.content) {
120230
120247
  if (c.type === "text") {
@@ -120265,6 +120282,10 @@ async function piStreamToOpenAiResponsesJson(piStream) {
120265
120282
  }
120266
120283
  };
120267
120284
  }
120285
+ async function piStreamToOpenAiResponsesJson(piStream) {
120286
+ const msg = await piStream.result();
120287
+ return openAiResponsesMessageFromAssistant(msg);
120288
+ }
120268
120289
 
120269
120290
  // src/pi-bridge/event-to-google.ts
120270
120291
  var encoder5 = new TextEncoder;
@@ -120381,8 +120402,7 @@ function piStreamToGoogleSse(piStream) {
120381
120402
  }
120382
120403
  });
120383
120404
  }
120384
- async function piStreamToGoogleJson(piStream) {
120385
- const msg = await piStream.result();
120405
+ function googleMessageFromAssistant(msg) {
120386
120406
  const parts = [];
120387
120407
  for (const c of msg.content) {
120388
120408
  if (c.type === "text") {
@@ -120414,6 +120434,10 @@ async function piStreamToGoogleJson(piStream) {
120414
120434
  modelVersion: msg.model || ""
120415
120435
  };
120416
120436
  }
120437
+ async function piStreamToGoogleJson(piStream) {
120438
+ const msg = await piStream.result();
120439
+ return googleMessageFromAssistant(msg);
120440
+ }
120417
120441
 
120418
120442
  // src/proxy/pipeline.ts
120419
120443
  registerAdapter(new AnthropicAdapter);
@@ -120650,35 +120674,12 @@ async function handleApiRequest(req, body, apiType, config, openclawConfig, auth
120650
120674
  }
120651
120675
  const detector = signalRouter.createSignalDetector();
120652
120676
  const detectionState = createSignalDetectionState();
120653
- if (wantsStream) {
120654
- const signalGen = detectSignalInStream(piStreamHandle, detector, detectionState, () => {});
120655
- const sseBody = piStreamToAnthropicSseFromGenerator(signalGen);
120656
- const response = new Response(sseBody, {
120657
- status: 200,
120658
- headers: { "content-type": "text/event-stream" }
120659
- });
120660
- response.clone().text().then(() => {
120661
- if (detectionState.signalDetected) {
120662
- abortCtrl.abort();
120663
- const nextTier = signalRouter.handleEscalation(messages, currentTier);
120664
- if (nextTier !== null) {
120665
- console.log(`[clawmux] [escalation] ${currentTier} → ${nextTier} (signal detected)`);
120666
- currentTier = nextTier;
120667
- return;
120668
- }
120669
- }
120670
- signalRouter.touchActivity(messages);
120671
- if (attempt > 0) {
120672
- signalRouter.recordSuccessfulEscalation(messages, currentTier);
120673
- }
120674
- });
120675
- return response;
120676
- }
120677
- const fullText = await collectPiStreamText(piStreamHandle, detector, detectionState);
120677
+ const assistantMsg = await collectPiStreamWithSignalDetection(piStreamHandle, detector, detectionState);
120678
120678
  if (detectionState.signalDetected) {
120679
+ abortCtrl.abort();
120679
120680
  const nextTier = signalRouter.handleEscalation(messages, currentTier);
120680
120681
  if (nextTier !== null) {
120681
- console.log(`[clawmux] [escalation] ${currentTier} → ${nextTier} (signal detected in non-streaming)`);
120682
+ console.log(`[clawmux] [escalation] ${currentTier} → ${nextTier} (signal detected)`);
120682
120683
  currentTier = nextTier;
120683
120684
  continue;
120684
120685
  }
@@ -120687,7 +120688,15 @@ async function handleApiRequest(req, body, apiType, config, openclawConfig, auth
120687
120688
  if (attempt > 0) {
120688
120689
  signalRouter.recordSuccessfulEscalation(messages, currentTier);
120689
120690
  }
120690
- return new Response(JSON.stringify(fullText), {
120691
+ if (wantsStream) {
120692
+ const sseBody = buildSseForApiType(apiType, replayAssistantMessageAsEvents(assistantMsg));
120693
+ return new Response(sseBody, {
120694
+ status: 200,
120695
+ headers: { "content-type": "text/event-stream" }
120696
+ });
120697
+ }
120698
+ const jsonBody = buildJsonForApiType(apiType, assistantMsg);
120699
+ return new Response(JSON.stringify(jsonBody), {
120691
120700
  status: 200,
120692
120701
  headers: { "content-type": "application/json" }
120693
120702
  });
@@ -120933,164 +120942,63 @@ async function yieldPiAiResponse(piStreamHandle, apiType, wantsStream) {
120933
120942
  }
120934
120943
  throw new Error(`Unsupported pi-ai apiType: ${apiType}`);
120935
120944
  }
120936
- function piStreamToAnthropicSseFromGenerator(gen) {
120937
- const encoder6 = new TextEncoder;
120938
- function sseFrame2(event, data) {
120939
- return encoder6.encode(`event: ${event}
120940
- data: ${JSON.stringify(data)}
120941
-
120942
- `);
120943
- }
120944
- return new ReadableStream({
120945
- async start(controller) {
120946
- try {
120947
- let messageStarted = false;
120948
- const openBlocks = new Map;
120949
- const ensureMessageStart = (model) => {
120950
- if (messageStarted)
120951
- return;
120952
- messageStarted = true;
120953
- controller.enqueue(sseFrame2("message_start", {
120954
- type: "message_start",
120955
- message: {
120956
- id: "msg_" + Date.now().toString(36),
120957
- type: "message",
120958
- role: "assistant",
120959
- content: [],
120960
- model,
120961
- stop_reason: null,
120962
- stop_sequence: null,
120963
- usage: { input_tokens: 0, output_tokens: 0 }
120964
- }
120965
- }));
120966
- };
120967
- for await (const event of gen) {
120968
- if (event.type === "start") {
120969
- ensureMessageStart(event.partial.model || "");
120970
- } else if (event.type === "text_start") {
120971
- ensureMessageStart(event.partial.model || "");
120972
- openBlocks.set(event.contentIndex, "text");
120973
- controller.enqueue(sseFrame2("content_block_start", {
120974
- type: "content_block_start",
120975
- index: event.contentIndex,
120976
- content_block: { type: "text", text: "" }
120977
- }));
120978
- } else if (event.type === "text_delta") {
120979
- if (openBlocks.get(event.contentIndex) !== "text") {
120980
- ensureMessageStart(event.partial.model || "");
120981
- openBlocks.set(event.contentIndex, "text");
120982
- controller.enqueue(sseFrame2("content_block_start", {
120983
- type: "content_block_start",
120984
- index: event.contentIndex,
120985
- content_block: { type: "text", text: "" }
120986
- }));
120987
- }
120988
- controller.enqueue(sseFrame2("content_block_delta", {
120989
- type: "content_block_delta",
120990
- index: event.contentIndex,
120991
- delta: { type: "text_delta", text: event.delta }
120992
- }));
120993
- } else if (event.type === "text_end") {
120994
- if (openBlocks.get(event.contentIndex) === "text") {
120995
- controller.enqueue(sseFrame2("content_block_stop", {
120996
- type: "content_block_stop",
120997
- index: event.contentIndex
120998
- }));
120999
- openBlocks.delete(event.contentIndex);
121000
- }
121001
- } else if (event.type === "done") {
121002
- for (const [idx] of openBlocks) {
121003
- controller.enqueue(sseFrame2("content_block_stop", {
121004
- type: "content_block_stop",
121005
- index: idx
121006
- }));
121007
- }
121008
- openBlocks.clear();
121009
- controller.enqueue(sseFrame2("message_delta", {
121010
- type: "message_delta",
121011
- delta: { stop_reason: "end_turn", stop_sequence: null },
121012
- usage: { output_tokens: event.message.usage?.output ?? 0 }
121013
- }));
121014
- controller.enqueue(sseFrame2("message_stop", { type: "message_stop" }));
121015
- } else if (event.type === "error") {
121016
- for (const [idx] of openBlocks) {
121017
- controller.enqueue(sseFrame2("content_block_stop", {
121018
- type: "content_block_stop",
121019
- index: idx
121020
- }));
121021
- }
121022
- openBlocks.clear();
121023
- controller.enqueue(sseFrame2("error", {
121024
- type: "error",
121025
- error: {
121026
- type: "api_error",
121027
- message: event.error?.errorMessage ?? "Unknown error"
121028
- }
121029
- }));
121030
- }
121031
- }
121032
- controller.close();
121033
- } catch (err) {
121034
- const msg = err instanceof Error ? err.message : String(err);
121035
- controller.enqueue(sseFrame2("error", {
121036
- type: "error",
121037
- error: { type: "api_error", message: msg }
121038
- }));
121039
- controller.close();
120945
+ function buildSseForApiType(apiType, gen) {
120946
+ if (apiType === "anthropic-messages")
120947
+ return piStreamToAnthropicSse(gen);
120948
+ if (apiType === "openai-completions")
120949
+ return piStreamToOpenAiCompletionsSse(gen);
120950
+ if (apiType === "openai-responses")
120951
+ return piStreamToOpenAiResponsesSse(gen);
120952
+ if (apiType === "google-generative-ai")
120953
+ return piStreamToGoogleSse(gen);
120954
+ return piStreamToAnthropicSse(gen);
120955
+ }
120956
+ function buildJsonForApiType(apiType, msg) {
120957
+ if (apiType === "anthropic-messages")
120958
+ return anthropicMessageFromAssistant(msg);
120959
+ if (apiType === "openai-completions")
120960
+ return openAiCompletionsMessageFromAssistant(msg);
120961
+ if (apiType === "openai-responses")
120962
+ return openAiResponsesMessageFromAssistant(msg);
120963
+ if (apiType === "google-generative-ai")
120964
+ return googleMessageFromAssistant(msg);
120965
+ return anthropicMessageFromAssistant(msg);
120966
+ }
120967
+ async function drainSignalGenerator(gen) {
120968
+ for await (const _event of gen) {}
120969
+ }
120970
+ async function* replayAssistantMessageAsEvents(msg) {
120971
+ const partial = msg;
120972
+ yield { type: "start", partial };
120973
+ for (let i2 = 0;i2 < msg.content.length; i2++) {
120974
+ const block = msg.content[i2];
120975
+ if (block.type === "text") {
120976
+ yield { type: "text_start", contentIndex: i2, partial };
120977
+ if (block.text.length > 0) {
120978
+ yield { type: "text_delta", contentIndex: i2, delta: block.text, partial };
121040
120979
  }
121041
- }
121042
- });
121043
- }
121044
- async function collectPiStreamText(piStreamHandle, detector, state) {
121045
- const msg = await piStreamHandle.result();
121046
- const fullText = msg.content.filter((c) => c.type === "text").map((c) => c.text).join("");
121047
- const results = detector.feedChunk(fullText);
121048
- for (const r2 of results) {
121049
- if (r2.type === "signal_detected") {
121050
- state.signalDetected = true;
121051
- break;
121052
- }
121053
- }
121054
- detector.flush();
121055
- const STOP_REASON_MAP2 = {
121056
- stop: "end_turn",
121057
- length: "max_tokens",
121058
- toolUse: "tool_use",
121059
- error: "end_turn",
121060
- aborted: "end_turn"
121061
- };
121062
- const blocks = [];
121063
- for (const c of msg.content) {
121064
- if (c.type === "text") {
121065
- blocks.push({ type: "text", text: c.text });
121066
- } else if (c.type === "thinking") {
121067
- blocks.push({
121068
- type: "thinking",
121069
- thinking: c.thinking,
121070
- ...c.thinkingSignature ? { signature: c.thinkingSignature } : {}
121071
- });
121072
- } else if (c.type === "toolCall") {
121073
- blocks.push({
121074
- type: "tool_use",
121075
- id: c.id,
121076
- name: c.name,
121077
- input: c.arguments ?? {}
121078
- });
120980
+ yield { type: "text_end", contentIndex: i2, content: block.text, partial };
120981
+ } else if (block.type === "thinking") {
120982
+ yield { type: "thinking_start", contentIndex: i2, partial };
120983
+ if (block.thinking.length > 0) {
120984
+ yield { type: "thinking_delta", contentIndex: i2, delta: block.thinking, partial };
120985
+ }
120986
+ yield { type: "thinking_end", contentIndex: i2, content: block.thinking, partial };
120987
+ } else if (block.type === "toolCall") {
120988
+ yield { type: "toolcall_start", contentIndex: i2, partial };
120989
+ const argsJson = JSON.stringify(block.arguments ?? {});
120990
+ if (argsJson.length > 0 && argsJson !== "{}") {
120991
+ yield { type: "toolcall_delta", contentIndex: i2, delta: argsJson, partial };
120992
+ }
120993
+ yield { type: "toolcall_end", contentIndex: i2, toolCall: block, partial };
121079
120994
  }
121080
120995
  }
121081
- return {
121082
- id: "msg_" + Date.now().toString(36),
121083
- type: "message",
121084
- role: "assistant",
121085
- content: blocks,
121086
- model: msg.model || "",
121087
- stop_reason: STOP_REASON_MAP2[msg.stopReason] ?? "end_turn",
121088
- stop_sequence: null,
121089
- usage: {
121090
- input_tokens: msg.usage?.input ?? 0,
121091
- output_tokens: msg.usage?.output ?? 0
121092
- }
121093
- };
120996
+ yield { type: "done", reason: msg.stopReason, message: msg };
120997
+ }
120998
+ async function collectPiStreamWithSignalDetection(piStreamHandle, detector, state) {
120999
+ const signalGen = detectSignalInStream(piStreamHandle, detector, state, () => {});
121000
+ await drainSignalGenerator(signalGen);
121001
+ return await piStreamHandle.result();
121094
121002
  }
121095
121003
  var ROUTE_MAPPINGS = [
121096
121004
  { apiType: "anthropic-messages", key: "/v1/messages" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmux",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "description": "Smart model routing + context compression proxy for OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {