clawmux 0.3.13 → 0.3.14

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
@@ -109,7 +109,7 @@ ClawMux resolves each model's context window using this priority chain:
109
109
 
110
110
  1. **~/.openclaw/clawmux.json** `routing.contextWindows` — explicit per-model override
111
111
  2. **openclaw.json** `models.providers[provider].models[].contextWindow` — user config
112
- 3. **OpenClaw built-in catalog** — pi-ai model database (812+ models)
112
+ 3. **OpenClaw built-in catalog** — pi-ai model database (830+ models)
113
113
  4. **Default: 200,000 tokens**
114
114
 
115
115
  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
@@ -112220,7 +112220,7 @@ var import_node_child_process = require("node:child_process");
112220
112220
  var import_node_os2 = require("node:os");
112221
112221
 
112222
112222
  // src/proxy/router.ts
112223
- var VERSION = process.env.npm_package_version ?? "0.3.13";
112223
+ var VERSION = process.env.npm_package_version ?? "0.3.14";
112224
112224
  function jsonResponse(body, status = 200) {
112225
112225
  return new Response(JSON.stringify(body), {
112226
112226
  status,
@@ -115091,6 +115091,9 @@ function djb2Hash(text) {
115091
115091
  return (hash >>> 0).toString(16);
115092
115092
  }
115093
115093
  function extractText(content) {
115094
+ if (content === null || content === undefined) {
115095
+ return "";
115096
+ }
115094
115097
  if (typeof content === "string") {
115095
115098
  return content.slice(0, 200);
115096
115099
  }
@@ -115103,6 +115106,9 @@ function extractText(content) {
115103
115106
  return concatenated.slice(0, 200);
115104
115107
  }
115105
115108
  function contentLength(content) {
115109
+ if (content === null || content === undefined) {
115110
+ return 0;
115111
+ }
115106
115112
  if (typeof content === "string") {
115107
115113
  return content.length;
115108
115114
  }
@@ -115178,7 +115184,7 @@ class EscalationMemory {
115178
115184
  }
115179
115185
 
115180
115186
  // src/routing/instruction-injector.ts
115181
- var INJECT_FOR_TIERS = new Set(["LIGHT"]);
115187
+ var INJECT_FOR_TIERS = new Set(["LIGHT", "MEDIUM"]);
115182
115188
  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
115189
  Do not explain. Do not ask permission. Just emit the marker and stop.`;
115184
115190
  function injectEscalationInstruction(messages) {
@@ -115194,6 +115200,12 @@ function injectEscalationInstruction(messages) {
115194
115200
  ...rest
115195
115201
  ];
115196
115202
  }
115203
+ if (first.content === null) {
115204
+ return [
115205
+ { role: "system", content: ESCALATION_INSTRUCTION },
115206
+ ...rest
115207
+ ];
115208
+ }
115197
115209
  if (typeof first.content === "string") {
115198
115210
  return [
115199
115211
  { role: "system", content: first.content + `
@@ -119730,8 +119742,7 @@ function piStreamToAnthropicSse(piStream) {
119730
119742
  }
119731
119743
  });
119732
119744
  }
119733
- async function piStreamToAnthropicJson(piStream) {
119734
- const msg = await piStream.result();
119745
+ function anthropicMessageFromAssistant(msg) {
119735
119746
  if ((msg.stopReason === "error" || msg.stopReason === "aborted") && msg.errorMessage) {
119736
119747
  console.error(`[clawmux] pi-ai upstream error (${msg.stopReason}): ${msg.errorMessage}`);
119737
119748
  }
@@ -119769,6 +119780,10 @@ async function piStreamToAnthropicJson(piStream) {
119769
119780
  ...msg.errorMessage ? { error_message: msg.errorMessage } : {}
119770
119781
  };
119771
119782
  }
119783
+ async function piStreamToAnthropicJson(piStream) {
119784
+ const msg = await piStream.result();
119785
+ return anthropicMessageFromAssistant(msg);
119786
+ }
119772
119787
 
119773
119788
  // src/pi-bridge/event-to-openai-completions.ts
119774
119789
  var encoder3 = new TextEncoder;
@@ -119992,8 +120007,7 @@ function piStreamToOpenAiCompletionsSse(piStream) {
119992
120007
  }
119993
120008
  });
119994
120009
  }
119995
- async function piStreamToOpenAiCompletionsJson(piStream) {
119996
- const msg = await piStream.result();
120010
+ function openAiCompletionsMessageFromAssistant(msg) {
119997
120011
  let textContent = "";
119998
120012
  const toolCalls = [];
119999
120013
  let reasoning = "";
@@ -120043,6 +120057,10 @@ async function piStreamToOpenAiCompletionsJson(piStream) {
120043
120057
  }
120044
120058
  };
120045
120059
  }
120060
+ async function piStreamToOpenAiCompletionsJson(piStream) {
120061
+ const msg = await piStream.result();
120062
+ return openAiCompletionsMessageFromAssistant(msg);
120063
+ }
120046
120064
 
120047
120065
  // src/pi-bridge/event-to-openai-responses.ts
120048
120066
  var encoder4 = new TextEncoder;
@@ -120205,8 +120223,7 @@ function piStreamToOpenAiResponsesSse(piStream) {
120205
120223
  }
120206
120224
  });
120207
120225
  }
120208
- async function piStreamToOpenAiResponsesJson(piStream) {
120209
- const msg = await piStream.result();
120226
+ function openAiResponsesMessageFromAssistant(msg) {
120210
120227
  const output = [];
120211
120228
  for (const c of msg.content) {
120212
120229
  if (c.type === "text") {
@@ -120247,6 +120264,10 @@ async function piStreamToOpenAiResponsesJson(piStream) {
120247
120264
  }
120248
120265
  };
120249
120266
  }
120267
+ async function piStreamToOpenAiResponsesJson(piStream) {
120268
+ const msg = await piStream.result();
120269
+ return openAiResponsesMessageFromAssistant(msg);
120270
+ }
120250
120271
 
120251
120272
  // src/pi-bridge/event-to-google.ts
120252
120273
  var encoder5 = new TextEncoder;
@@ -120363,8 +120384,7 @@ function piStreamToGoogleSse(piStream) {
120363
120384
  }
120364
120385
  });
120365
120386
  }
120366
- async function piStreamToGoogleJson(piStream) {
120367
- const msg = await piStream.result();
120387
+ function googleMessageFromAssistant(msg) {
120368
120388
  const parts = [];
120369
120389
  for (const c of msg.content) {
120370
120390
  if (c.type === "text") {
@@ -120396,6 +120416,10 @@ async function piStreamToGoogleJson(piStream) {
120396
120416
  modelVersion: msg.model || ""
120397
120417
  };
120398
120418
  }
120419
+ async function piStreamToGoogleJson(piStream) {
120420
+ const msg = await piStream.result();
120421
+ return googleMessageFromAssistant(msg);
120422
+ }
120399
120423
 
120400
120424
  // src/proxy/pipeline.ts
120401
120425
  registerAdapter(new AnthropicAdapter);
@@ -120632,35 +120656,12 @@ async function handleApiRequest(req, body, apiType, config, openclawConfig, auth
120632
120656
  }
120633
120657
  const detector = signalRouter.createSignalDetector();
120634
120658
  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);
120659
+ const assistantMsg = await collectPiStreamWithSignalDetection(piStreamHandle, detector, detectionState);
120660
120660
  if (detectionState.signalDetected) {
120661
+ abortCtrl.abort();
120661
120662
  const nextTier = signalRouter.handleEscalation(messages, currentTier);
120662
120663
  if (nextTier !== null) {
120663
- console.log(`[clawmux] [escalation] ${currentTier} → ${nextTier} (signal detected in non-streaming)`);
120664
+ console.log(`[clawmux] [escalation] ${currentTier} → ${nextTier} (signal detected)`);
120664
120665
  currentTier = nextTier;
120665
120666
  continue;
120666
120667
  }
@@ -120669,7 +120670,15 @@ async function handleApiRequest(req, body, apiType, config, openclawConfig, auth
120669
120670
  if (attempt > 0) {
120670
120671
  signalRouter.recordSuccessfulEscalation(messages, currentTier);
120671
120672
  }
120672
- return new Response(JSON.stringify(fullText), {
120673
+ if (wantsStream) {
120674
+ const sseBody = buildSseForApiType(apiType, replayAssistantMessageAsEvents(assistantMsg));
120675
+ return new Response(sseBody, {
120676
+ status: 200,
120677
+ headers: { "content-type": "text/event-stream" }
120678
+ });
120679
+ }
120680
+ const jsonBody = buildJsonForApiType(apiType, assistantMsg);
120681
+ return new Response(JSON.stringify(jsonBody), {
120673
120682
  status: 200,
120674
120683
  headers: { "content-type": "application/json" }
120675
120684
  });
@@ -120915,164 +120924,63 @@ async function yieldPiAiResponse(piStreamHandle, apiType, wantsStream) {
120915
120924
  }
120916
120925
  throw new Error(`Unsupported pi-ai apiType: ${apiType}`);
120917
120926
  }
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();
120927
+ function buildSseForApiType(apiType, gen) {
120928
+ if (apiType === "anthropic-messages")
120929
+ return piStreamToAnthropicSse(gen);
120930
+ if (apiType === "openai-completions")
120931
+ return piStreamToOpenAiCompletionsSse(gen);
120932
+ if (apiType === "openai-responses")
120933
+ return piStreamToOpenAiResponsesSse(gen);
120934
+ if (apiType === "google-generative-ai")
120935
+ return piStreamToGoogleSse(gen);
120936
+ return piStreamToAnthropicSse(gen);
120937
+ }
120938
+ function buildJsonForApiType(apiType, msg) {
120939
+ if (apiType === "anthropic-messages")
120940
+ return anthropicMessageFromAssistant(msg);
120941
+ if (apiType === "openai-completions")
120942
+ return openAiCompletionsMessageFromAssistant(msg);
120943
+ if (apiType === "openai-responses")
120944
+ return openAiResponsesMessageFromAssistant(msg);
120945
+ if (apiType === "google-generative-ai")
120946
+ return googleMessageFromAssistant(msg);
120947
+ return anthropicMessageFromAssistant(msg);
120948
+ }
120949
+ async function drainSignalGenerator(gen) {
120950
+ for await (const _event of gen) {}
120951
+ }
120952
+ async function* replayAssistantMessageAsEvents(msg) {
120953
+ const partial = msg;
120954
+ yield { type: "start", partial };
120955
+ for (let i2 = 0;i2 < msg.content.length; i2++) {
120956
+ const block = msg.content[i2];
120957
+ if (block.type === "text") {
120958
+ yield { type: "text_start", contentIndex: i2, partial };
120959
+ if (block.text.length > 0) {
120960
+ yield { type: "text_delta", contentIndex: i2, delta: block.text, partial };
121022
120961
  }
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
- });
120962
+ yield { type: "text_end", contentIndex: i2, content: block.text, partial };
120963
+ } else if (block.type === "thinking") {
120964
+ yield { type: "thinking_start", contentIndex: i2, partial };
120965
+ if (block.thinking.length > 0) {
120966
+ yield { type: "thinking_delta", contentIndex: i2, delta: block.thinking, partial };
120967
+ }
120968
+ yield { type: "thinking_end", contentIndex: i2, content: block.thinking, partial };
120969
+ } else if (block.type === "toolCall") {
120970
+ yield { type: "toolcall_start", contentIndex: i2, partial };
120971
+ const argsJson = JSON.stringify(block.arguments ?? {});
120972
+ if (argsJson.length > 0 && argsJson !== "{}") {
120973
+ yield { type: "toolcall_delta", contentIndex: i2, delta: argsJson, partial };
120974
+ }
120975
+ yield { type: "toolcall_end", contentIndex: i2, toolCall: block, partial };
121061
120976
  }
121062
120977
  }
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
- };
120978
+ yield { type: "done", reason: msg.stopReason, message: msg };
120979
+ }
120980
+ async function collectPiStreamWithSignalDetection(piStreamHandle, detector, state) {
120981
+ const signalGen = detectSignalInStream(piStreamHandle, detector, state, () => {});
120982
+ await drainSignalGenerator(signalGen);
120983
+ return await piStreamHandle.result();
121076
120984
  }
121077
120985
  var ROUTE_MAPPINGS = [
121078
120986
  { apiType: "anthropic-messages", key: "/v1/messages" },
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.14";
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.14",
4
4
  "description": "Smart model routing + context compression proxy for OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {