clawmux 0.3.1 → 0.3.2

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
@@ -14,19 +14,6 @@ Smart model routing + context compression proxy for OpenClaw.
14
14
 
15
15
  ## Installation
16
16
 
17
- ### For Humans
18
-
19
- Copy and paste this prompt to your LLM agent (OpenClaw, Claude Code, etc.):
20
-
21
- ```
22
- Read the following guide and execute every step without asking for confirmation:
23
- https://raw.githubusercontent.com/HyeokjaeLee/ClawMux/refs/heads/main/docs/guide/installation.md
24
- ```
25
-
26
- Or install manually, but seriously, let an agent do it. Agents don't fat-finger configs.
27
-
28
- ### For OpenClaw Agents
29
-
30
17
  Fetch the installation guide and follow it:
31
18
 
32
19
  ```bash
package/dist/cli.cjs CHANGED
@@ -586,14 +586,7 @@ function getAuthProfilesPath(agentId) {
586
586
  const id = agentId ?? "main";
587
587
  return import_node_path2.join(getHomeDir(), ".openclaw", "agents", id, "agent", "auth-profiles.json");
588
588
  }
589
- async function readAuthProfiles(agentId, profilesPath) {
590
- const path = profilesPath ?? getAuthProfilesPath(agentId);
591
- let text;
592
- try {
593
- text = await import_promises4.readFile(path, "utf-8");
594
- } catch {
595
- return [];
596
- }
589
+ function parseAuthProfilesFile(text) {
597
590
  try {
598
591
  const parsed = JSON.parse(text);
599
592
  if (Array.isArray(parsed))
@@ -601,7 +594,7 @@ async function readAuthProfiles(agentId, profilesPath) {
601
594
  if (parsed && typeof parsed === "object" && parsed.profiles) {
602
595
  return Object.entries(parsed.profiles).map(([key, profile]) => ({
603
596
  provider: profile.provider ?? key.split(":")[0],
604
- apiKey: profile.access ?? profile.apiKey,
597
+ apiKey: profile.access ?? profile.apiKey ?? profile.key,
605
598
  token: profile.token
606
599
  })).filter((p) => {
607
600
  const token = p.apiKey ?? p.token;
@@ -624,6 +617,33 @@ async function readAuthProfiles(agentId, profilesPath) {
624
617
  throw new Error(`Failed to parse auth-profiles.json: ${message}`);
625
618
  }
626
619
  }
620
+ async function readAuthProfiles(_agentId, _profilesPath, agentsDirOverride) {
621
+ const agentsDir = agentsDirOverride ?? import_node_path2.join(getHomeDir(), ".openclaw", "agents");
622
+ let agentDirs;
623
+ try {
624
+ agentDirs = (await import_promises4.readdir(agentsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name).sort();
625
+ } catch {
626
+ const path = getAuthProfilesPath(_agentId);
627
+ try {
628
+ return parseAuthProfilesFile(await import_promises4.readFile(path, "utf-8"));
629
+ } catch {
630
+ return [];
631
+ }
632
+ }
633
+ const ordered = ["main", ...agentDirs.filter((d) => d !== "main")];
634
+ const merged = new Map;
635
+ for (const agentId of ordered) {
636
+ const profilePath = import_node_path2.join(agentsDir, agentId, "agent", "auth-profiles.json");
637
+ try {
638
+ const text = await import_promises4.readFile(profilePath, "utf-8");
639
+ const profiles = parseAuthProfilesFile(text);
640
+ for (const p of profiles) {
641
+ merged.set(p.provider, p);
642
+ }
643
+ } catch {}
644
+ }
645
+ return Array.from(merged.values());
646
+ }
627
647
  function getProviderConfig(provider, config) {
628
648
  return config.models?.providers?.[provider];
629
649
  }
@@ -772,6 +792,38 @@ function getAdapter(apiType) {
772
792
  return adapters.get(apiType);
773
793
  }
774
794
 
795
+ // src/adapters/tool-converter.ts
796
+ function toOpenAITools(tools) {
797
+ if (!Array.isArray(tools) || tools.length === 0)
798
+ return;
799
+ return tools.map((tool) => {
800
+ if (tool.type === "function" && tool.function)
801
+ return tool;
802
+ return {
803
+ type: "function",
804
+ function: {
805
+ name: tool.name ?? "",
806
+ description: tool.description ?? "",
807
+ parameters: tool.input_schema ?? tool.parameters ?? { type: "object", properties: {} }
808
+ }
809
+ };
810
+ });
811
+ }
812
+ function toAnthropicTools(tools) {
813
+ if (!Array.isArray(tools) || tools.length === 0)
814
+ return;
815
+ return tools.map((tool) => {
816
+ if (tool.input_schema && !tool.type)
817
+ return tool;
818
+ const fn = tool.function ?? tool;
819
+ return {
820
+ name: fn.name ?? "",
821
+ description: fn.description ?? "",
822
+ input_schema: fn.parameters ?? { type: "object", properties: {} }
823
+ };
824
+ });
825
+ }
826
+
775
827
  // src/adapters/anthropic.ts
776
828
  class AnthropicAdapter {
777
829
  apiType = "anthropic-messages";
@@ -798,18 +850,42 @@ class AnthropicAdapter {
798
850
  "anthropic-version": "2023-06-01",
799
851
  "content-type": "application/json"
800
852
  };
801
- let bodyObj = {
802
- ...parsed.rawBody,
803
- model: targetModel
804
- };
805
853
  const isHaiku = targetModel.toLowerCase().includes("haiku");
806
854
  const hasThinking = "thinking" in parsed.rawBody;
807
855
  if (hasThinking && !isHaiku) {
808
856
  headers["anthropic-beta"] = "interleaved-thinking-2025-05-14";
809
857
  }
810
- if (isHaiku && "thinking" in bodyObj) {
811
- const { thinking: _, ...rest } = bodyObj;
812
- bodyObj = rest;
858
+ const ANTHROPIC_SAMPLING_KEYS = [
859
+ "temperature",
860
+ "top_p",
861
+ "top_k",
862
+ "stop_sequences",
863
+ "metadata",
864
+ "service_tier"
865
+ ];
866
+ const samplingParams = {};
867
+ for (const key of ANTHROPIC_SAMPLING_KEYS) {
868
+ if (key in parsed.rawBody) {
869
+ samplingParams[key] = parsed.rawBody[key];
870
+ }
871
+ }
872
+ let bodyObj = {
873
+ model: targetModel,
874
+ messages: parsed.messages,
875
+ stream: parsed.stream,
876
+ ...samplingParams
877
+ };
878
+ if (parsed.system !== undefined) {
879
+ bodyObj.system = parsed.system;
880
+ }
881
+ if (parsed.maxTokens !== undefined) {
882
+ bodyObj.max_tokens = parsed.maxTokens;
883
+ }
884
+ if (parsed.rawBody.tools) {
885
+ bodyObj.tools = toAnthropicTools(parsed.rawBody.tools);
886
+ }
887
+ if (!isHaiku && hasThinking) {
888
+ bodyObj.thinking = parsed.rawBody.thinking;
813
889
  }
814
890
  return {
815
891
  url,
@@ -1076,11 +1152,43 @@ class OpenAICompletionsAdapter {
1076
1152
  return parseOpenAIBody(body);
1077
1153
  }
1078
1154
  buildUpstreamRequest(parsed, targetModel, baseUrl, auth) {
1079
- const { rawBody } = parsed;
1155
+ const messages = [];
1156
+ if (parsed.system !== undefined) {
1157
+ messages.push({ role: "system", content: parsed.system });
1158
+ }
1159
+ messages.push(...parsed.messages);
1160
+ const OPENAI_SAMPLING_KEYS = [
1161
+ "temperature",
1162
+ "top_p",
1163
+ "frequency_penalty",
1164
+ "presence_penalty",
1165
+ "logprobs",
1166
+ "top_logprobs",
1167
+ "seed",
1168
+ "stop",
1169
+ "n",
1170
+ "logit_bias",
1171
+ "response_format",
1172
+ "reasoning_effort"
1173
+ ];
1174
+ const samplingParams = {};
1175
+ for (const key of OPENAI_SAMPLING_KEYS) {
1176
+ if (key in parsed.rawBody) {
1177
+ samplingParams[key] = parsed.rawBody[key];
1178
+ }
1179
+ }
1080
1180
  const upstreamBody = {
1081
- ...rawBody,
1082
- model: targetModel
1181
+ model: targetModel,
1182
+ messages,
1183
+ stream: parsed.stream,
1184
+ ...samplingParams
1083
1185
  };
1186
+ if (parsed.maxTokens !== undefined) {
1187
+ upstreamBody.max_tokens = parsed.maxTokens;
1188
+ }
1189
+ if (parsed.rawBody.tools) {
1190
+ upstreamBody.tools = toOpenAITools(parsed.rawBody.tools);
1191
+ }
1084
1192
  return {
1085
1193
  url: /\/v\d+\/?$/.test(baseUrl) ? `${baseUrl.replace(/\/$/, "")}/chat/completions` : `${baseUrl}/v1/chat/completions`,
1086
1194
  method: "POST",
@@ -1107,8 +1215,11 @@ class OpenAICompletionsAdapter {
1107
1215
  if (Array.isArray(choices) && choices.length > 0) {
1108
1216
  const choice = choices[0];
1109
1217
  const message = choice.message;
1110
- if (message && typeof message.content === "string") {
1111
- content = message.content;
1218
+ if (message) {
1219
+ const messageText = message.content ?? message.reasoning_content;
1220
+ if (typeof messageText === "string") {
1221
+ content = messageText;
1222
+ }
1112
1223
  }
1113
1224
  if (typeof choice.finish_reason === "string") {
1114
1225
  stopReason = choice.finish_reason;
@@ -1178,16 +1289,17 @@ class OpenAICompletionsAdapter {
1178
1289
  const choice = choices[0];
1179
1290
  const delta = choice.delta;
1180
1291
  const finishReason = choice.finish_reason;
1181
- if (delta?.role === "assistant" && !delta.content) {
1292
+ const textContent = delta?.content ?? delta?.reasoning_content;
1293
+ if (delta?.role === "assistant" && textContent == null) {
1182
1294
  events.push({
1183
1295
  type: "message_start",
1184
1296
  id: String(data.id ?? ""),
1185
1297
  model: String(data.model ?? "")
1186
1298
  });
1187
- } else if (typeof delta?.content === "string") {
1299
+ } else if (typeof textContent === "string") {
1188
1300
  events.push({
1189
1301
  type: "content_delta",
1190
- text: delta.content,
1302
+ text: textContent,
1191
1303
  index: typeof choice.index === "number" ? choice.index : 0
1192
1304
  });
1193
1305
  }
@@ -1219,7 +1331,7 @@ class OpenAICompletionsAdapter {
1219
1331
  choices: [
1220
1332
  {
1221
1333
  index: 0,
1222
- delta: { role: "assistant", content: "" },
1334
+ delta: { role: "assistant" },
1223
1335
  finish_reason: null
1224
1336
  }
1225
1337
  ]
@@ -1279,11 +1391,40 @@ class OpenAIResponsesAdapter {
1279
1391
  return parseOpenAIBody(body);
1280
1392
  }
1281
1393
  buildUpstreamRequest(parsed, targetModel, baseUrl, auth) {
1282
- const { rawBody } = parsed;
1394
+ const input = [];
1395
+ if (parsed.system !== undefined) {
1396
+ input.push({ role: "system", content: parsed.system });
1397
+ }
1398
+ input.push(...parsed.messages);
1399
+ const OPENAI_RESPONSES_SAMPLING_KEYS = [
1400
+ "temperature",
1401
+ "top_p",
1402
+ "truncation",
1403
+ "reasoning",
1404
+ "reasoning_effort",
1405
+ "text",
1406
+ "metadata",
1407
+ "store",
1408
+ "include"
1409
+ ];
1410
+ const samplingParams = {};
1411
+ for (const key of OPENAI_RESPONSES_SAMPLING_KEYS) {
1412
+ if (key in parsed.rawBody) {
1413
+ samplingParams[key] = parsed.rawBody[key];
1414
+ }
1415
+ }
1283
1416
  const upstreamBody = {
1284
- ...rawBody,
1285
- model: targetModel
1417
+ model: targetModel,
1418
+ input,
1419
+ stream: parsed.stream,
1420
+ ...samplingParams
1286
1421
  };
1422
+ if (parsed.maxTokens !== undefined) {
1423
+ upstreamBody.max_output_tokens = parsed.maxTokens;
1424
+ }
1425
+ if (parsed.rawBody.tools) {
1426
+ upstreamBody.tools = toOpenAITools(parsed.rawBody.tools);
1427
+ }
1287
1428
  return {
1288
1429
  url: `${baseUrl}/v1/responses`,
1289
1430
  method: "POST",
@@ -2292,6 +2433,9 @@ class OpenAICodexAdapter {
2292
2433
  }
2293
2434
  delete upstreamBody.max_tokens;
2294
2435
  delete upstreamBody.max_output_tokens;
2436
+ if (upstreamBody.tools) {
2437
+ upstreamBody.tools = toOpenAITools(upstreamBody.tools);
2438
+ }
2295
2439
  return {
2296
2440
  url: `${baseUrl}/codex/responses`,
2297
2441
  method: "POST",
@@ -2948,6 +3092,7 @@ function createStreamTranslator(sourceAdapter, targetAdapter) {
2948
3092
  return new TransformStream;
2949
3093
  }
2950
3094
  let buffer = "";
3095
+ let messageStarted = false;
2951
3096
  return new TransformStream({
2952
3097
  transform(chunk, controller) {
2953
3098
  if (!sourceAdapter.parseStreamChunk || !targetAdapter.buildStreamChunk) {
@@ -2965,6 +3110,18 @@ function createStreamTranslator(sourceAdapter, targetAdapter) {
2965
3110
  continue;
2966
3111
  const events = sourceAdapter.parseStreamChunk(frame);
2967
3112
  for (const event of events) {
3113
+ if (event.type === "message_start") {
3114
+ messageStarted = true;
3115
+ } else if (!messageStarted && (event.type === "content_delta" || event.type === "content_stop")) {
3116
+ messageStarted = true;
3117
+ const synthetic = targetAdapter.buildStreamChunk({
3118
+ type: "message_start",
3119
+ id: "",
3120
+ model: ""
3121
+ });
3122
+ if (synthetic)
3123
+ controller.enqueue(encoder.encode(synthetic));
3124
+ }
2968
3125
  const translated = targetAdapter.buildStreamChunk(event);
2969
3126
  controller.enqueue(encoder.encode(translated));
2970
3127
  }
@@ -2974,6 +3131,18 @@ function createStreamTranslator(sourceAdapter, targetAdapter) {
2974
3131
  if (buffer.trim() !== "" && sourceAdapter.parseStreamChunk && targetAdapter.buildStreamChunk) {
2975
3132
  const events = sourceAdapter.parseStreamChunk(buffer);
2976
3133
  for (const event of events) {
3134
+ if (event.type === "message_start") {
3135
+ messageStarted = true;
3136
+ } else if (!messageStarted && (event.type === "content_delta" || event.type === "content_stop")) {
3137
+ messageStarted = true;
3138
+ const synthetic = targetAdapter.buildStreamChunk({
3139
+ type: "message_start",
3140
+ id: "",
3141
+ model: ""
3142
+ });
3143
+ if (synthetic)
3144
+ controller.enqueue(encoder.encode(synthetic));
3145
+ }
2977
3146
  const translated = targetAdapter.buildStreamChunk(event);
2978
3147
  controller.enqueue(encoder.encode(translated));
2979
3148
  }
@@ -3769,7 +3938,7 @@ function getLogDir() {
3769
3938
  }
3770
3939
 
3771
3940
  // src/cli.ts
3772
- var VERSION2 = process.env.npm_package_version ?? "0.3.1";
3941
+ var VERSION2 = process.env.npm_package_version ?? "0.3.2";
3773
3942
  var SERVICE_NAME = "clawmux";
3774
3943
  var HELP = `Usage: clawmux <command>
3775
3944
 
@@ -3791,7 +3960,17 @@ Environment:
3791
3960
  CLAWMUX_PORT Server port override
3792
3961
  OPENCLAW_CONFIG_PATH Path to openclaw.json`;
3793
3962
  var PROVIDER_KEY = "clawmux";
3794
- var PROVIDER_API = "anthropic-messages";
3963
+ var PROVIDER_API_FALLBACK = "openai-responses";
3964
+ function resolveProviderApi(mediumModel, openclawProviders) {
3965
+ const providerName = mediumModel.split("/")[0];
3966
+ if (!providerName)
3967
+ return PROVIDER_API_FALLBACK;
3968
+ const providerConfig = openclawProviders[providerName];
3969
+ const api = providerConfig?.["api"];
3970
+ if (typeof api === "string" && api.length > 0)
3971
+ return api;
3972
+ return PROVIDER_API_FALLBACK;
3973
+ }
3795
3974
  async function fileExistsLocal(path) {
3796
3975
  try {
3797
3976
  await import_promises6.access(path);
@@ -4083,17 +4262,41 @@ async function init() {
4083
4262
  if (!models.providers)
4084
4263
  models.providers = {};
4085
4264
  const providers = models.providers;
4265
+ let providerApi = PROVIDER_API_FALLBACK;
4266
+ try {
4267
+ const clawmuxRaw = await import_promises6.readFile(clawmuxJsonPath, "utf-8");
4268
+ const clawmuxConfig = JSON.parse(clawmuxRaw);
4269
+ const routing = clawmuxConfig["routing"];
4270
+ const routingModels = routing?.["models"];
4271
+ const mediumModel = routingModels?.["MEDIUM"];
4272
+ if (typeof mediumModel === "string" && mediumModel.length > 0) {
4273
+ providerApi = resolveProviderApi(mediumModel, providers);
4274
+ console.log(`[info] MEDIUM model: ${mediumModel} → provider api: ${providerApi}`);
4275
+ } else {
4276
+ console.log(`[info] MEDIUM model not configured yet, using default api: ${providerApi}`);
4277
+ }
4278
+ } catch {
4279
+ console.log(`[info] clawmux.json not readable, using default api: ${providerApi}`);
4280
+ }
4086
4281
  if (providers[PROVIDER_KEY]) {
4087
- console.log(` skip ${PROVIDER_KEY} (already exists)`);
4282
+ const existing = providers[PROVIDER_KEY];
4283
+ if (existing["api"] !== providerApi) {
4284
+ existing["api"] = providerApi;
4285
+ await import_promises6.writeFile(openclawConfigPath, JSON.stringify(config, null, 2) + `
4286
+ `);
4287
+ console.log(` updated ${PROVIDER_KEY} provider api → ${providerApi}`);
4288
+ } else {
4289
+ console.log(` skip ${PROVIDER_KEY} (already exists, api=${providerApi})`);
4290
+ }
4088
4291
  } else {
4089
4292
  providers[PROVIDER_KEY] = {
4090
4293
  baseUrl: "http://localhost:3456",
4091
- api: PROVIDER_API,
4294
+ api: providerApi,
4092
4295
  models: [{ id: "auto", name: "ClawMux Auto Router" }]
4093
4296
  };
4094
4297
  await import_promises6.writeFile(openclawConfigPath, JSON.stringify(config, null, 2) + `
4095
4298
  `);
4096
- console.log(` added ${PROVIDER_KEY} provider to openclaw.json`);
4299
+ console.log(` added ${PROVIDER_KEY} provider to openclaw.json (api=${providerApi})`);
4097
4300
  }
4098
4301
  const port = process.env.CLAWMUX_PORT ?? "3456";
4099
4302
  if (!noService) {
package/dist/index.cjs CHANGED
@@ -609,14 +609,7 @@ function getAuthProfilesPath(agentId) {
609
609
  const id = agentId ?? "main";
610
610
  return import_node_path2.join(getHomeDir(), ".openclaw", "agents", id, "agent", "auth-profiles.json");
611
611
  }
612
- async function readAuthProfiles(agentId, profilesPath) {
613
- const path = profilesPath ?? getAuthProfilesPath(agentId);
614
- let text;
615
- try {
616
- text = await import_promises4.readFile(path, "utf-8");
617
- } catch {
618
- return [];
619
- }
612
+ function parseAuthProfilesFile(text) {
620
613
  try {
621
614
  const parsed = JSON.parse(text);
622
615
  if (Array.isArray(parsed))
@@ -624,7 +617,7 @@ async function readAuthProfiles(agentId, profilesPath) {
624
617
  if (parsed && typeof parsed === "object" && parsed.profiles) {
625
618
  return Object.entries(parsed.profiles).map(([key, profile]) => ({
626
619
  provider: profile.provider ?? key.split(":")[0],
627
- apiKey: profile.access ?? profile.apiKey,
620
+ apiKey: profile.access ?? profile.apiKey ?? profile.key,
628
621
  token: profile.token
629
622
  })).filter((p) => {
630
623
  const token = p.apiKey ?? p.token;
@@ -647,6 +640,33 @@ async function readAuthProfiles(agentId, profilesPath) {
647
640
  throw new Error(`Failed to parse auth-profiles.json: ${message}`);
648
641
  }
649
642
  }
643
+ async function readAuthProfiles(_agentId, _profilesPath, agentsDirOverride) {
644
+ const agentsDir = agentsDirOverride ?? import_node_path2.join(getHomeDir(), ".openclaw", "agents");
645
+ let agentDirs;
646
+ try {
647
+ agentDirs = (await import_promises4.readdir(agentsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name).sort();
648
+ } catch {
649
+ const path = getAuthProfilesPath(_agentId);
650
+ try {
651
+ return parseAuthProfilesFile(await import_promises4.readFile(path, "utf-8"));
652
+ } catch {
653
+ return [];
654
+ }
655
+ }
656
+ const ordered = ["main", ...agentDirs.filter((d) => d !== "main")];
657
+ const merged = new Map;
658
+ for (const agentId of ordered) {
659
+ const profilePath = import_node_path2.join(agentsDir, agentId, "agent", "auth-profiles.json");
660
+ try {
661
+ const text = await import_promises4.readFile(profilePath, "utf-8");
662
+ const profiles = parseAuthProfilesFile(text);
663
+ for (const p of profiles) {
664
+ merged.set(p.provider, p);
665
+ }
666
+ } catch {}
667
+ }
668
+ return Array.from(merged.values());
669
+ }
650
670
  function getProviderConfig(provider, config) {
651
671
  return config.models?.providers?.[provider];
652
672
  }
@@ -795,6 +815,38 @@ function getAdapter(apiType) {
795
815
  return adapters.get(apiType);
796
816
  }
797
817
 
818
+ // src/adapters/tool-converter.ts
819
+ function toOpenAITools(tools) {
820
+ if (!Array.isArray(tools) || tools.length === 0)
821
+ return;
822
+ return tools.map((tool) => {
823
+ if (tool.type === "function" && tool.function)
824
+ return tool;
825
+ return {
826
+ type: "function",
827
+ function: {
828
+ name: tool.name ?? "",
829
+ description: tool.description ?? "",
830
+ parameters: tool.input_schema ?? tool.parameters ?? { type: "object", properties: {} }
831
+ }
832
+ };
833
+ });
834
+ }
835
+ function toAnthropicTools(tools) {
836
+ if (!Array.isArray(tools) || tools.length === 0)
837
+ return;
838
+ return tools.map((tool) => {
839
+ if (tool.input_schema && !tool.type)
840
+ return tool;
841
+ const fn = tool.function ?? tool;
842
+ return {
843
+ name: fn.name ?? "",
844
+ description: fn.description ?? "",
845
+ input_schema: fn.parameters ?? { type: "object", properties: {} }
846
+ };
847
+ });
848
+ }
849
+
798
850
  // src/adapters/anthropic.ts
799
851
  class AnthropicAdapter {
800
852
  apiType = "anthropic-messages";
@@ -821,18 +873,42 @@ class AnthropicAdapter {
821
873
  "anthropic-version": "2023-06-01",
822
874
  "content-type": "application/json"
823
875
  };
824
- let bodyObj = {
825
- ...parsed.rawBody,
826
- model: targetModel
827
- };
828
876
  const isHaiku = targetModel.toLowerCase().includes("haiku");
829
877
  const hasThinking = "thinking" in parsed.rawBody;
830
878
  if (hasThinking && !isHaiku) {
831
879
  headers["anthropic-beta"] = "interleaved-thinking-2025-05-14";
832
880
  }
833
- if (isHaiku && "thinking" in bodyObj) {
834
- const { thinking: _, ...rest } = bodyObj;
835
- bodyObj = rest;
881
+ const ANTHROPIC_SAMPLING_KEYS = [
882
+ "temperature",
883
+ "top_p",
884
+ "top_k",
885
+ "stop_sequences",
886
+ "metadata",
887
+ "service_tier"
888
+ ];
889
+ const samplingParams = {};
890
+ for (const key of ANTHROPIC_SAMPLING_KEYS) {
891
+ if (key in parsed.rawBody) {
892
+ samplingParams[key] = parsed.rawBody[key];
893
+ }
894
+ }
895
+ let bodyObj = {
896
+ model: targetModel,
897
+ messages: parsed.messages,
898
+ stream: parsed.stream,
899
+ ...samplingParams
900
+ };
901
+ if (parsed.system !== undefined) {
902
+ bodyObj.system = parsed.system;
903
+ }
904
+ if (parsed.maxTokens !== undefined) {
905
+ bodyObj.max_tokens = parsed.maxTokens;
906
+ }
907
+ if (parsed.rawBody.tools) {
908
+ bodyObj.tools = toAnthropicTools(parsed.rawBody.tools);
909
+ }
910
+ if (!isHaiku && hasThinking) {
911
+ bodyObj.thinking = parsed.rawBody.thinking;
836
912
  }
837
913
  return {
838
914
  url,
@@ -1099,11 +1175,43 @@ class OpenAICompletionsAdapter {
1099
1175
  return parseOpenAIBody(body);
1100
1176
  }
1101
1177
  buildUpstreamRequest(parsed, targetModel, baseUrl, auth) {
1102
- const { rawBody } = parsed;
1178
+ const messages = [];
1179
+ if (parsed.system !== undefined) {
1180
+ messages.push({ role: "system", content: parsed.system });
1181
+ }
1182
+ messages.push(...parsed.messages);
1183
+ const OPENAI_SAMPLING_KEYS = [
1184
+ "temperature",
1185
+ "top_p",
1186
+ "frequency_penalty",
1187
+ "presence_penalty",
1188
+ "logprobs",
1189
+ "top_logprobs",
1190
+ "seed",
1191
+ "stop",
1192
+ "n",
1193
+ "logit_bias",
1194
+ "response_format",
1195
+ "reasoning_effort"
1196
+ ];
1197
+ const samplingParams = {};
1198
+ for (const key of OPENAI_SAMPLING_KEYS) {
1199
+ if (key in parsed.rawBody) {
1200
+ samplingParams[key] = parsed.rawBody[key];
1201
+ }
1202
+ }
1103
1203
  const upstreamBody = {
1104
- ...rawBody,
1105
- model: targetModel
1204
+ model: targetModel,
1205
+ messages,
1206
+ stream: parsed.stream,
1207
+ ...samplingParams
1106
1208
  };
1209
+ if (parsed.maxTokens !== undefined) {
1210
+ upstreamBody.max_tokens = parsed.maxTokens;
1211
+ }
1212
+ if (parsed.rawBody.tools) {
1213
+ upstreamBody.tools = toOpenAITools(parsed.rawBody.tools);
1214
+ }
1107
1215
  return {
1108
1216
  url: /\/v\d+\/?$/.test(baseUrl) ? `${baseUrl.replace(/\/$/, "")}/chat/completions` : `${baseUrl}/v1/chat/completions`,
1109
1217
  method: "POST",
@@ -1130,8 +1238,11 @@ class OpenAICompletionsAdapter {
1130
1238
  if (Array.isArray(choices) && choices.length > 0) {
1131
1239
  const choice = choices[0];
1132
1240
  const message = choice.message;
1133
- if (message && typeof message.content === "string") {
1134
- content = message.content;
1241
+ if (message) {
1242
+ const messageText = message.content ?? message.reasoning_content;
1243
+ if (typeof messageText === "string") {
1244
+ content = messageText;
1245
+ }
1135
1246
  }
1136
1247
  if (typeof choice.finish_reason === "string") {
1137
1248
  stopReason = choice.finish_reason;
@@ -1201,16 +1312,17 @@ class OpenAICompletionsAdapter {
1201
1312
  const choice = choices[0];
1202
1313
  const delta = choice.delta;
1203
1314
  const finishReason = choice.finish_reason;
1204
- if (delta?.role === "assistant" && !delta.content) {
1315
+ const textContent = delta?.content ?? delta?.reasoning_content;
1316
+ if (delta?.role === "assistant" && textContent == null) {
1205
1317
  events.push({
1206
1318
  type: "message_start",
1207
1319
  id: String(data.id ?? ""),
1208
1320
  model: String(data.model ?? "")
1209
1321
  });
1210
- } else if (typeof delta?.content === "string") {
1322
+ } else if (typeof textContent === "string") {
1211
1323
  events.push({
1212
1324
  type: "content_delta",
1213
- text: delta.content,
1325
+ text: textContent,
1214
1326
  index: typeof choice.index === "number" ? choice.index : 0
1215
1327
  });
1216
1328
  }
@@ -1242,7 +1354,7 @@ class OpenAICompletionsAdapter {
1242
1354
  choices: [
1243
1355
  {
1244
1356
  index: 0,
1245
- delta: { role: "assistant", content: "" },
1357
+ delta: { role: "assistant" },
1246
1358
  finish_reason: null
1247
1359
  }
1248
1360
  ]
@@ -1302,11 +1414,40 @@ class OpenAIResponsesAdapter {
1302
1414
  return parseOpenAIBody(body);
1303
1415
  }
1304
1416
  buildUpstreamRequest(parsed, targetModel, baseUrl, auth) {
1305
- const { rawBody } = parsed;
1417
+ const input = [];
1418
+ if (parsed.system !== undefined) {
1419
+ input.push({ role: "system", content: parsed.system });
1420
+ }
1421
+ input.push(...parsed.messages);
1422
+ const OPENAI_RESPONSES_SAMPLING_KEYS = [
1423
+ "temperature",
1424
+ "top_p",
1425
+ "truncation",
1426
+ "reasoning",
1427
+ "reasoning_effort",
1428
+ "text",
1429
+ "metadata",
1430
+ "store",
1431
+ "include"
1432
+ ];
1433
+ const samplingParams = {};
1434
+ for (const key of OPENAI_RESPONSES_SAMPLING_KEYS) {
1435
+ if (key in parsed.rawBody) {
1436
+ samplingParams[key] = parsed.rawBody[key];
1437
+ }
1438
+ }
1306
1439
  const upstreamBody = {
1307
- ...rawBody,
1308
- model: targetModel
1440
+ model: targetModel,
1441
+ input,
1442
+ stream: parsed.stream,
1443
+ ...samplingParams
1309
1444
  };
1445
+ if (parsed.maxTokens !== undefined) {
1446
+ upstreamBody.max_output_tokens = parsed.maxTokens;
1447
+ }
1448
+ if (parsed.rawBody.tools) {
1449
+ upstreamBody.tools = toOpenAITools(parsed.rawBody.tools);
1450
+ }
1310
1451
  return {
1311
1452
  url: `${baseUrl}/v1/responses`,
1312
1453
  method: "POST",
@@ -2315,6 +2456,9 @@ class OpenAICodexAdapter {
2315
2456
  }
2316
2457
  delete upstreamBody.max_tokens;
2317
2458
  delete upstreamBody.max_output_tokens;
2459
+ if (upstreamBody.tools) {
2460
+ upstreamBody.tools = toOpenAITools(upstreamBody.tools);
2461
+ }
2318
2462
  return {
2319
2463
  url: `${baseUrl}/codex/responses`,
2320
2464
  method: "POST",
@@ -2971,6 +3115,7 @@ function createStreamTranslator(sourceAdapter, targetAdapter) {
2971
3115
  return new TransformStream;
2972
3116
  }
2973
3117
  let buffer = "";
3118
+ let messageStarted = false;
2974
3119
  return new TransformStream({
2975
3120
  transform(chunk, controller) {
2976
3121
  if (!sourceAdapter.parseStreamChunk || !targetAdapter.buildStreamChunk) {
@@ -2988,6 +3133,18 @@ function createStreamTranslator(sourceAdapter, targetAdapter) {
2988
3133
  continue;
2989
3134
  const events = sourceAdapter.parseStreamChunk(frame);
2990
3135
  for (const event of events) {
3136
+ if (event.type === "message_start") {
3137
+ messageStarted = true;
3138
+ } else if (!messageStarted && (event.type === "content_delta" || event.type === "content_stop")) {
3139
+ messageStarted = true;
3140
+ const synthetic = targetAdapter.buildStreamChunk({
3141
+ type: "message_start",
3142
+ id: "",
3143
+ model: ""
3144
+ });
3145
+ if (synthetic)
3146
+ controller.enqueue(encoder.encode(synthetic));
3147
+ }
2991
3148
  const translated = targetAdapter.buildStreamChunk(event);
2992
3149
  controller.enqueue(encoder.encode(translated));
2993
3150
  }
@@ -2997,6 +3154,18 @@ function createStreamTranslator(sourceAdapter, targetAdapter) {
2997
3154
  if (buffer.trim() !== "" && sourceAdapter.parseStreamChunk && targetAdapter.buildStreamChunk) {
2998
3155
  const events = sourceAdapter.parseStreamChunk(buffer);
2999
3156
  for (const event of events) {
3157
+ if (event.type === "message_start") {
3158
+ messageStarted = true;
3159
+ } else if (!messageStarted && (event.type === "content_delta" || event.type === "content_stop")) {
3160
+ messageStarted = true;
3161
+ const synthetic = targetAdapter.buildStreamChunk({
3162
+ type: "message_start",
3163
+ id: "",
3164
+ model: ""
3165
+ });
3166
+ if (synthetic)
3167
+ controller.enqueue(encoder.encode(synthetic));
3168
+ }
3000
3169
  const translated = targetAdapter.buildStreamChunk(event);
3001
3170
  controller.enqueue(encoder.encode(translated));
3002
3171
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmux",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Smart model routing + context compression proxy for OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {