clawmux 0.3.1 → 0.3.3
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 +10 -16
- package/dist/cli.cjs +269 -61
- package/dist/index.cjs +210 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,28 +14,22 @@ Smart model routing + context compression proxy for OpenClaw.
|
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Copy and paste this into your OpenClaw agent:
|
|
18
18
|
|
|
19
|
-
|
|
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
|
|
19
|
+
```bash
|
|
20
|
+
Install and configure oh-my-opencode by following the instructions here:
|
|
21
|
+
curl -s https://raw.githubusercontent.com/HyeokjaeLee/ClawMux/refs/heads/main/docs/guide/installation.md
|
|
24
22
|
```
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
### For OpenClaw Agents
|
|
24
|
+
## Configuration
|
|
29
25
|
|
|
30
|
-
|
|
26
|
+
ClawMux stores its config at `~/.openclaw/clawmux.json` (next to `openclaw.json`). `clawmux init` creates it automatically. You can also copy `clawmux.example.json` as a starting point:
|
|
31
27
|
|
|
32
28
|
```bash
|
|
33
|
-
|
|
29
|
+
cp clawmux.example.json ~/.openclaw/clawmux.json
|
|
34
30
|
```
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Copy `clawmux.example.json` to `clawmux.json` and adjust as needed:
|
|
32
|
+
Adjust as needed:
|
|
39
33
|
|
|
40
34
|
```jsonc
|
|
41
35
|
{
|
|
@@ -59,7 +53,7 @@ Copy `clawmux.example.json` to `clawmux.json` and adjust as needed:
|
|
|
59
53
|
}
|
|
60
54
|
```
|
|
61
55
|
|
|
62
|
-
Config is watched for changes. Edit
|
|
56
|
+
Config is watched for changes. Edit `~/.openclaw/clawmux.json` while the proxy is running and it reloads automatically. Override the path with `CLAWMUX_CONFIG=/path/to/clawmux.json`.
|
|
63
57
|
|
|
64
58
|
### Cross-Provider Routing
|
|
65
59
|
|
|
@@ -112,7 +106,7 @@ OpenClaw → ClawMux Proxy (localhost:3456) → Upstream Provider(s)
|
|
|
112
106
|
|
|
113
107
|
ClawMux resolves each model's context window using this priority chain:
|
|
114
108
|
|
|
115
|
-
1.
|
|
109
|
+
1. **~/.openclaw/clawmux.json** `routing.contextWindows` — explicit per-model override
|
|
116
110
|
2. **openclaw.json** `models.providers[provider].models[].contextWindow` — user config
|
|
117
111
|
3. **OpenClaw built-in catalog** — pi-ai model database (812+ models)
|
|
118
112
|
4. **Default: 200,000 tokens**
|
package/dist/cli.cjs
CHANGED
|
@@ -80,7 +80,7 @@ async function writeWebResponse(res, response) {
|
|
|
80
80
|
|
|
81
81
|
// src/cli.ts
|
|
82
82
|
var import_promises6 = require("node:fs/promises");
|
|
83
|
-
var
|
|
83
|
+
var import_node_path5 = require("node:path");
|
|
84
84
|
var import_node_child_process = require("node:child_process");
|
|
85
85
|
var import_node_os = require("node:os");
|
|
86
86
|
|
|
@@ -424,12 +424,17 @@ function validateConfig(raw) {
|
|
|
424
424
|
}
|
|
425
425
|
|
|
426
426
|
// src/config/loader.ts
|
|
427
|
-
function
|
|
427
|
+
function getHomeDir() {
|
|
428
|
+
return process.env.HOME ?? "/root";
|
|
429
|
+
}
|
|
430
|
+
function getClawmuxConfigPath() {
|
|
428
431
|
const envPath = process.env.CLAWMUX_CONFIG;
|
|
429
|
-
if (envPath)
|
|
432
|
+
if (envPath)
|
|
430
433
|
return import_node_path.resolve(envPath);
|
|
431
|
-
|
|
432
|
-
|
|
434
|
+
return import_node_path.join(getHomeDir(), ".openclaw", "clawmux.json");
|
|
435
|
+
}
|
|
436
|
+
function findConfigPath() {
|
|
437
|
+
return getClawmuxConfigPath();
|
|
433
438
|
}
|
|
434
439
|
async function loadConfig(configPath) {
|
|
435
440
|
const filePath = configPath ?? findConfigPath();
|
|
@@ -553,7 +558,7 @@ function resolveEnvVar(value) {
|
|
|
553
558
|
}
|
|
554
559
|
return value;
|
|
555
560
|
}
|
|
556
|
-
function
|
|
561
|
+
function getHomeDir2() {
|
|
557
562
|
return process.env.HOME ?? "/root";
|
|
558
563
|
}
|
|
559
564
|
function getConfigPath(override) {
|
|
@@ -561,7 +566,7 @@ function getConfigPath(override) {
|
|
|
561
566
|
return override;
|
|
562
567
|
if (process.env.OPENCLAW_CONFIG_PATH)
|
|
563
568
|
return process.env.OPENCLAW_CONFIG_PATH;
|
|
564
|
-
return import_node_path2.join(
|
|
569
|
+
return import_node_path2.join(getHomeDir2(), ".openclaw", "openclaw.json");
|
|
565
570
|
}
|
|
566
571
|
async function readOpenClawConfig(configPath) {
|
|
567
572
|
const path = getConfigPath(configPath);
|
|
@@ -584,16 +589,9 @@ async function readOpenClawConfig(configPath) {
|
|
|
584
589
|
}
|
|
585
590
|
function getAuthProfilesPath(agentId) {
|
|
586
591
|
const id = agentId ?? "main";
|
|
587
|
-
return import_node_path2.join(
|
|
592
|
+
return import_node_path2.join(getHomeDir2(), ".openclaw", "agents", id, "agent", "auth-profiles.json");
|
|
588
593
|
}
|
|
589
|
-
|
|
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
|
-
}
|
|
594
|
+
function parseAuthProfilesFile(text) {
|
|
597
595
|
try {
|
|
598
596
|
const parsed = JSON.parse(text);
|
|
599
597
|
if (Array.isArray(parsed))
|
|
@@ -601,7 +599,7 @@ async function readAuthProfiles(agentId, profilesPath) {
|
|
|
601
599
|
if (parsed && typeof parsed === "object" && parsed.profiles) {
|
|
602
600
|
return Object.entries(parsed.profiles).map(([key, profile]) => ({
|
|
603
601
|
provider: profile.provider ?? key.split(":")[0],
|
|
604
|
-
apiKey: profile.access ?? profile.apiKey,
|
|
602
|
+
apiKey: profile.access ?? profile.apiKey ?? profile.key,
|
|
605
603
|
token: profile.token
|
|
606
604
|
})).filter((p) => {
|
|
607
605
|
const token = p.apiKey ?? p.token;
|
|
@@ -624,6 +622,33 @@ async function readAuthProfiles(agentId, profilesPath) {
|
|
|
624
622
|
throw new Error(`Failed to parse auth-profiles.json: ${message}`);
|
|
625
623
|
}
|
|
626
624
|
}
|
|
625
|
+
async function readAuthProfiles(_agentId, _profilesPath, agentsDirOverride) {
|
|
626
|
+
const agentsDir = agentsDirOverride ?? import_node_path2.join(getHomeDir2(), ".openclaw", "agents");
|
|
627
|
+
let agentDirs;
|
|
628
|
+
try {
|
|
629
|
+
agentDirs = (await import_promises4.readdir(agentsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
630
|
+
} catch {
|
|
631
|
+
const path = getAuthProfilesPath(_agentId);
|
|
632
|
+
try {
|
|
633
|
+
return parseAuthProfilesFile(await import_promises4.readFile(path, "utf-8"));
|
|
634
|
+
} catch {
|
|
635
|
+
return [];
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
const ordered = ["main", ...agentDirs.filter((d) => d !== "main")];
|
|
639
|
+
const merged = new Map;
|
|
640
|
+
for (const agentId of ordered) {
|
|
641
|
+
const profilePath = import_node_path2.join(agentsDir, agentId, "agent", "auth-profiles.json");
|
|
642
|
+
try {
|
|
643
|
+
const text = await import_promises4.readFile(profilePath, "utf-8");
|
|
644
|
+
const profiles = parseAuthProfilesFile(text);
|
|
645
|
+
for (const p of profiles) {
|
|
646
|
+
merged.set(p.provider, p);
|
|
647
|
+
}
|
|
648
|
+
} catch {}
|
|
649
|
+
}
|
|
650
|
+
return Array.from(merged.values());
|
|
651
|
+
}
|
|
627
652
|
function getProviderConfig(provider, config) {
|
|
628
653
|
return config.models?.providers?.[provider];
|
|
629
654
|
}
|
|
@@ -772,6 +797,38 @@ function getAdapter(apiType) {
|
|
|
772
797
|
return adapters.get(apiType);
|
|
773
798
|
}
|
|
774
799
|
|
|
800
|
+
// src/adapters/tool-converter.ts
|
|
801
|
+
function toOpenAITools(tools) {
|
|
802
|
+
if (!Array.isArray(tools) || tools.length === 0)
|
|
803
|
+
return;
|
|
804
|
+
return tools.map((tool) => {
|
|
805
|
+
if (tool.type === "function" && tool.function)
|
|
806
|
+
return tool;
|
|
807
|
+
return {
|
|
808
|
+
type: "function",
|
|
809
|
+
function: {
|
|
810
|
+
name: tool.name ?? "",
|
|
811
|
+
description: tool.description ?? "",
|
|
812
|
+
parameters: tool.input_schema ?? tool.parameters ?? { type: "object", properties: {} }
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
function toAnthropicTools(tools) {
|
|
818
|
+
if (!Array.isArray(tools) || tools.length === 0)
|
|
819
|
+
return;
|
|
820
|
+
return tools.map((tool) => {
|
|
821
|
+
if (tool.input_schema && !tool.type)
|
|
822
|
+
return tool;
|
|
823
|
+
const fn = tool.function ?? tool;
|
|
824
|
+
return {
|
|
825
|
+
name: fn.name ?? "",
|
|
826
|
+
description: fn.description ?? "",
|
|
827
|
+
input_schema: fn.parameters ?? { type: "object", properties: {} }
|
|
828
|
+
};
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
775
832
|
// src/adapters/anthropic.ts
|
|
776
833
|
class AnthropicAdapter {
|
|
777
834
|
apiType = "anthropic-messages";
|
|
@@ -798,18 +855,42 @@ class AnthropicAdapter {
|
|
|
798
855
|
"anthropic-version": "2023-06-01",
|
|
799
856
|
"content-type": "application/json"
|
|
800
857
|
};
|
|
801
|
-
let bodyObj = {
|
|
802
|
-
...parsed.rawBody,
|
|
803
|
-
model: targetModel
|
|
804
|
-
};
|
|
805
858
|
const isHaiku = targetModel.toLowerCase().includes("haiku");
|
|
806
859
|
const hasThinking = "thinking" in parsed.rawBody;
|
|
807
860
|
if (hasThinking && !isHaiku) {
|
|
808
861
|
headers["anthropic-beta"] = "interleaved-thinking-2025-05-14";
|
|
809
862
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
863
|
+
const ANTHROPIC_SAMPLING_KEYS = [
|
|
864
|
+
"temperature",
|
|
865
|
+
"top_p",
|
|
866
|
+
"top_k",
|
|
867
|
+
"stop_sequences",
|
|
868
|
+
"metadata",
|
|
869
|
+
"service_tier"
|
|
870
|
+
];
|
|
871
|
+
const samplingParams = {};
|
|
872
|
+
for (const key of ANTHROPIC_SAMPLING_KEYS) {
|
|
873
|
+
if (key in parsed.rawBody) {
|
|
874
|
+
samplingParams[key] = parsed.rawBody[key];
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
let bodyObj = {
|
|
878
|
+
model: targetModel,
|
|
879
|
+
messages: parsed.messages,
|
|
880
|
+
stream: parsed.stream,
|
|
881
|
+
...samplingParams
|
|
882
|
+
};
|
|
883
|
+
if (parsed.system !== undefined) {
|
|
884
|
+
bodyObj.system = parsed.system;
|
|
885
|
+
}
|
|
886
|
+
if (parsed.maxTokens !== undefined) {
|
|
887
|
+
bodyObj.max_tokens = parsed.maxTokens;
|
|
888
|
+
}
|
|
889
|
+
if (parsed.rawBody.tools) {
|
|
890
|
+
bodyObj.tools = toAnthropicTools(parsed.rawBody.tools);
|
|
891
|
+
}
|
|
892
|
+
if (!isHaiku && hasThinking) {
|
|
893
|
+
bodyObj.thinking = parsed.rawBody.thinking;
|
|
813
894
|
}
|
|
814
895
|
return {
|
|
815
896
|
url,
|
|
@@ -1076,11 +1157,43 @@ class OpenAICompletionsAdapter {
|
|
|
1076
1157
|
return parseOpenAIBody(body);
|
|
1077
1158
|
}
|
|
1078
1159
|
buildUpstreamRequest(parsed, targetModel, baseUrl, auth) {
|
|
1079
|
-
const
|
|
1160
|
+
const messages = [];
|
|
1161
|
+
if (parsed.system !== undefined) {
|
|
1162
|
+
messages.push({ role: "system", content: parsed.system });
|
|
1163
|
+
}
|
|
1164
|
+
messages.push(...parsed.messages);
|
|
1165
|
+
const OPENAI_SAMPLING_KEYS = [
|
|
1166
|
+
"temperature",
|
|
1167
|
+
"top_p",
|
|
1168
|
+
"frequency_penalty",
|
|
1169
|
+
"presence_penalty",
|
|
1170
|
+
"logprobs",
|
|
1171
|
+
"top_logprobs",
|
|
1172
|
+
"seed",
|
|
1173
|
+
"stop",
|
|
1174
|
+
"n",
|
|
1175
|
+
"logit_bias",
|
|
1176
|
+
"response_format",
|
|
1177
|
+
"reasoning_effort"
|
|
1178
|
+
];
|
|
1179
|
+
const samplingParams = {};
|
|
1180
|
+
for (const key of OPENAI_SAMPLING_KEYS) {
|
|
1181
|
+
if (key in parsed.rawBody) {
|
|
1182
|
+
samplingParams[key] = parsed.rawBody[key];
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1080
1185
|
const upstreamBody = {
|
|
1081
|
-
|
|
1082
|
-
|
|
1186
|
+
model: targetModel,
|
|
1187
|
+
messages,
|
|
1188
|
+
stream: parsed.stream,
|
|
1189
|
+
...samplingParams
|
|
1083
1190
|
};
|
|
1191
|
+
if (parsed.maxTokens !== undefined) {
|
|
1192
|
+
upstreamBody.max_tokens = parsed.maxTokens;
|
|
1193
|
+
}
|
|
1194
|
+
if (parsed.rawBody.tools) {
|
|
1195
|
+
upstreamBody.tools = toOpenAITools(parsed.rawBody.tools);
|
|
1196
|
+
}
|
|
1084
1197
|
return {
|
|
1085
1198
|
url: /\/v\d+\/?$/.test(baseUrl) ? `${baseUrl.replace(/\/$/, "")}/chat/completions` : `${baseUrl}/v1/chat/completions`,
|
|
1086
1199
|
method: "POST",
|
|
@@ -1107,8 +1220,11 @@ class OpenAICompletionsAdapter {
|
|
|
1107
1220
|
if (Array.isArray(choices) && choices.length > 0) {
|
|
1108
1221
|
const choice = choices[0];
|
|
1109
1222
|
const message = choice.message;
|
|
1110
|
-
if (message
|
|
1111
|
-
|
|
1223
|
+
if (message) {
|
|
1224
|
+
const messageText = message.content ?? message.reasoning_content;
|
|
1225
|
+
if (typeof messageText === "string") {
|
|
1226
|
+
content = messageText;
|
|
1227
|
+
}
|
|
1112
1228
|
}
|
|
1113
1229
|
if (typeof choice.finish_reason === "string") {
|
|
1114
1230
|
stopReason = choice.finish_reason;
|
|
@@ -1178,16 +1294,17 @@ class OpenAICompletionsAdapter {
|
|
|
1178
1294
|
const choice = choices[0];
|
|
1179
1295
|
const delta = choice.delta;
|
|
1180
1296
|
const finishReason = choice.finish_reason;
|
|
1181
|
-
|
|
1297
|
+
const textContent = delta?.content ?? delta?.reasoning_content;
|
|
1298
|
+
if (delta?.role === "assistant" && textContent == null) {
|
|
1182
1299
|
events.push({
|
|
1183
1300
|
type: "message_start",
|
|
1184
1301
|
id: String(data.id ?? ""),
|
|
1185
1302
|
model: String(data.model ?? "")
|
|
1186
1303
|
});
|
|
1187
|
-
} else if (typeof
|
|
1304
|
+
} else if (typeof textContent === "string") {
|
|
1188
1305
|
events.push({
|
|
1189
1306
|
type: "content_delta",
|
|
1190
|
-
text:
|
|
1307
|
+
text: textContent,
|
|
1191
1308
|
index: typeof choice.index === "number" ? choice.index : 0
|
|
1192
1309
|
});
|
|
1193
1310
|
}
|
|
@@ -1219,7 +1336,7 @@ class OpenAICompletionsAdapter {
|
|
|
1219
1336
|
choices: [
|
|
1220
1337
|
{
|
|
1221
1338
|
index: 0,
|
|
1222
|
-
delta: { role: "assistant"
|
|
1339
|
+
delta: { role: "assistant" },
|
|
1223
1340
|
finish_reason: null
|
|
1224
1341
|
}
|
|
1225
1342
|
]
|
|
@@ -1279,11 +1396,40 @@ class OpenAIResponsesAdapter {
|
|
|
1279
1396
|
return parseOpenAIBody(body);
|
|
1280
1397
|
}
|
|
1281
1398
|
buildUpstreamRequest(parsed, targetModel, baseUrl, auth) {
|
|
1282
|
-
const
|
|
1399
|
+
const input = [];
|
|
1400
|
+
if (parsed.system !== undefined) {
|
|
1401
|
+
input.push({ role: "system", content: parsed.system });
|
|
1402
|
+
}
|
|
1403
|
+
input.push(...parsed.messages);
|
|
1404
|
+
const OPENAI_RESPONSES_SAMPLING_KEYS = [
|
|
1405
|
+
"temperature",
|
|
1406
|
+
"top_p",
|
|
1407
|
+
"truncation",
|
|
1408
|
+
"reasoning",
|
|
1409
|
+
"reasoning_effort",
|
|
1410
|
+
"text",
|
|
1411
|
+
"metadata",
|
|
1412
|
+
"store",
|
|
1413
|
+
"include"
|
|
1414
|
+
];
|
|
1415
|
+
const samplingParams = {};
|
|
1416
|
+
for (const key of OPENAI_RESPONSES_SAMPLING_KEYS) {
|
|
1417
|
+
if (key in parsed.rawBody) {
|
|
1418
|
+
samplingParams[key] = parsed.rawBody[key];
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1283
1421
|
const upstreamBody = {
|
|
1284
|
-
|
|
1285
|
-
|
|
1422
|
+
model: targetModel,
|
|
1423
|
+
input,
|
|
1424
|
+
stream: parsed.stream,
|
|
1425
|
+
...samplingParams
|
|
1286
1426
|
};
|
|
1427
|
+
if (parsed.maxTokens !== undefined) {
|
|
1428
|
+
upstreamBody.max_output_tokens = parsed.maxTokens;
|
|
1429
|
+
}
|
|
1430
|
+
if (parsed.rawBody.tools) {
|
|
1431
|
+
upstreamBody.tools = toOpenAITools(parsed.rawBody.tools);
|
|
1432
|
+
}
|
|
1287
1433
|
return {
|
|
1288
1434
|
url: `${baseUrl}/v1/responses`,
|
|
1289
1435
|
method: "POST",
|
|
@@ -2292,6 +2438,9 @@ class OpenAICodexAdapter {
|
|
|
2292
2438
|
}
|
|
2293
2439
|
delete upstreamBody.max_tokens;
|
|
2294
2440
|
delete upstreamBody.max_output_tokens;
|
|
2441
|
+
if (upstreamBody.tools) {
|
|
2442
|
+
upstreamBody.tools = toOpenAITools(upstreamBody.tools);
|
|
2443
|
+
}
|
|
2295
2444
|
return {
|
|
2296
2445
|
url: `${baseUrl}/codex/responses`,
|
|
2297
2446
|
method: "POST",
|
|
@@ -2948,6 +3097,7 @@ function createStreamTranslator(sourceAdapter, targetAdapter) {
|
|
|
2948
3097
|
return new TransformStream;
|
|
2949
3098
|
}
|
|
2950
3099
|
let buffer = "";
|
|
3100
|
+
let messageStarted = false;
|
|
2951
3101
|
return new TransformStream({
|
|
2952
3102
|
transform(chunk, controller) {
|
|
2953
3103
|
if (!sourceAdapter.parseStreamChunk || !targetAdapter.buildStreamChunk) {
|
|
@@ -2965,6 +3115,18 @@ function createStreamTranslator(sourceAdapter, targetAdapter) {
|
|
|
2965
3115
|
continue;
|
|
2966
3116
|
const events = sourceAdapter.parseStreamChunk(frame);
|
|
2967
3117
|
for (const event of events) {
|
|
3118
|
+
if (event.type === "message_start") {
|
|
3119
|
+
messageStarted = true;
|
|
3120
|
+
} else if (!messageStarted && (event.type === "content_delta" || event.type === "content_stop")) {
|
|
3121
|
+
messageStarted = true;
|
|
3122
|
+
const synthetic = targetAdapter.buildStreamChunk({
|
|
3123
|
+
type: "message_start",
|
|
3124
|
+
id: "",
|
|
3125
|
+
model: ""
|
|
3126
|
+
});
|
|
3127
|
+
if (synthetic)
|
|
3128
|
+
controller.enqueue(encoder.encode(synthetic));
|
|
3129
|
+
}
|
|
2968
3130
|
const translated = targetAdapter.buildStreamChunk(event);
|
|
2969
3131
|
controller.enqueue(encoder.encode(translated));
|
|
2970
3132
|
}
|
|
@@ -2974,6 +3136,18 @@ function createStreamTranslator(sourceAdapter, targetAdapter) {
|
|
|
2974
3136
|
if (buffer.trim() !== "" && sourceAdapter.parseStreamChunk && targetAdapter.buildStreamChunk) {
|
|
2975
3137
|
const events = sourceAdapter.parseStreamChunk(buffer);
|
|
2976
3138
|
for (const event of events) {
|
|
3139
|
+
if (event.type === "message_start") {
|
|
3140
|
+
messageStarted = true;
|
|
3141
|
+
} else if (!messageStarted && (event.type === "content_delta" || event.type === "content_stop")) {
|
|
3142
|
+
messageStarted = true;
|
|
3143
|
+
const synthetic = targetAdapter.buildStreamChunk({
|
|
3144
|
+
type: "message_start",
|
|
3145
|
+
id: "",
|
|
3146
|
+
model: ""
|
|
3147
|
+
});
|
|
3148
|
+
if (synthetic)
|
|
3149
|
+
controller.enqueue(encoder.encode(synthetic));
|
|
3150
|
+
}
|
|
2977
3151
|
const translated = targetAdapter.buildStreamChunk(event);
|
|
2978
3152
|
controller.enqueue(encoder.encode(translated));
|
|
2979
3153
|
}
|
|
@@ -3664,9 +3838,8 @@ function setupPipelineRoutes(config, openclawConfig, authProfiles, compressionMi
|
|
|
3664
3838
|
}
|
|
3665
3839
|
|
|
3666
3840
|
// src/index.ts
|
|
3667
|
-
var import_node_path4 = require("node:path");
|
|
3668
3841
|
async function bootstrap(portOverride) {
|
|
3669
|
-
const configPath =
|
|
3842
|
+
const configPath = getClawmuxConfigPath();
|
|
3670
3843
|
const result = await loadConfig(configPath);
|
|
3671
3844
|
if (!result.valid) {
|
|
3672
3845
|
console.error("[clawmux] Config errors:");
|
|
@@ -3701,8 +3874,8 @@ if (typeof Bun !== "undefined" && Bun.main === "/home/runner/work/ClawMux/ClawMu
|
|
|
3701
3874
|
|
|
3702
3875
|
// src/utils/logger.ts
|
|
3703
3876
|
var import_node_fs2 = require("node:fs");
|
|
3704
|
-
var
|
|
3705
|
-
var LOG_DIR =
|
|
3877
|
+
var import_node_path4 = require("node:path");
|
|
3878
|
+
var LOG_DIR = import_node_path4.join(process.env.HOME ?? "/root", ".openclaw", "clawmux");
|
|
3706
3879
|
var MAX_DAYS = 7;
|
|
3707
3880
|
var fileStream = null;
|
|
3708
3881
|
var currentDate = "";
|
|
@@ -3711,7 +3884,7 @@ function todayString() {
|
|
|
3711
3884
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
3712
3885
|
}
|
|
3713
3886
|
function logPath(date) {
|
|
3714
|
-
return
|
|
3887
|
+
return import_node_path4.join(LOG_DIR, `${date}.log`);
|
|
3715
3888
|
}
|
|
3716
3889
|
function rotateIfNeeded() {
|
|
3717
3890
|
const today = todayString();
|
|
@@ -3729,7 +3902,7 @@ function purgeOldLogs() {
|
|
|
3729
3902
|
const files = import_node_fs2.readdirSync(LOG_DIR).filter((f) => f.endsWith(".log")).sort();
|
|
3730
3903
|
while (files.length > MAX_DAYS) {
|
|
3731
3904
|
const oldest = files.shift();
|
|
3732
|
-
import_node_fs2.unlinkSync(
|
|
3905
|
+
import_node_fs2.unlinkSync(import_node_path4.join(LOG_DIR, oldest));
|
|
3733
3906
|
}
|
|
3734
3907
|
} catch (_) {}
|
|
3735
3908
|
}
|
|
@@ -3769,7 +3942,7 @@ function getLogDir() {
|
|
|
3769
3942
|
}
|
|
3770
3943
|
|
|
3771
3944
|
// src/cli.ts
|
|
3772
|
-
var VERSION2 = process.env.npm_package_version ?? "0.3.
|
|
3945
|
+
var VERSION2 = process.env.npm_package_version ?? "0.3.3";
|
|
3773
3946
|
var SERVICE_NAME = "clawmux";
|
|
3774
3947
|
var HELP = `Usage: clawmux <command>
|
|
3775
3948
|
|
|
@@ -3791,7 +3964,17 @@ Environment:
|
|
|
3791
3964
|
CLAWMUX_PORT Server port override
|
|
3792
3965
|
OPENCLAW_CONFIG_PATH Path to openclaw.json`;
|
|
3793
3966
|
var PROVIDER_KEY = "clawmux";
|
|
3794
|
-
var
|
|
3967
|
+
var PROVIDER_API_FALLBACK = "openai-responses";
|
|
3968
|
+
function resolveProviderApi(mediumModel, openclawProviders) {
|
|
3969
|
+
const providerName = mediumModel.split("/")[0];
|
|
3970
|
+
if (!providerName)
|
|
3971
|
+
return PROVIDER_API_FALLBACK;
|
|
3972
|
+
const providerConfig = openclawProviders[providerName];
|
|
3973
|
+
const api = providerConfig?.["api"];
|
|
3974
|
+
if (typeof api === "string" && api.length > 0)
|
|
3975
|
+
return api;
|
|
3976
|
+
return PROVIDER_API_FALLBACK;
|
|
3977
|
+
}
|
|
3795
3978
|
async function fileExistsLocal(path) {
|
|
3796
3979
|
try {
|
|
3797
3980
|
await import_promises6.access(path);
|
|
@@ -3819,13 +4002,13 @@ function resolveClawmuxBin() {
|
|
|
3819
4002
|
return detectPackageManager() === "bunx" ? "bunx clawmux" : "npx clawmux";
|
|
3820
4003
|
}
|
|
3821
4004
|
}
|
|
3822
|
-
function
|
|
4005
|
+
function getHomeDir3() {
|
|
3823
4006
|
return process.env.HOME ?? "/root";
|
|
3824
4007
|
}
|
|
3825
|
-
var SYSTEMD_DIR =
|
|
3826
|
-
var SYSTEMD_PATH =
|
|
3827
|
-
var LAUNCHD_DIR =
|
|
3828
|
-
var LAUNCHD_PATH =
|
|
4008
|
+
var SYSTEMD_DIR = import_node_path5.join(getHomeDir3(), ".config", "systemd", "user");
|
|
4009
|
+
var SYSTEMD_PATH = import_node_path5.join(SYSTEMD_DIR, `${SERVICE_NAME}.service`);
|
|
4010
|
+
var LAUNCHD_DIR = import_node_path5.join(getHomeDir3(), "Library", "LaunchAgents");
|
|
4011
|
+
var LAUNCHD_PATH = import_node_path5.join(LAUNCHD_DIR, `com.${SERVICE_NAME}.plist`);
|
|
3829
4012
|
function buildSystemdUnit(bin, port, workDir) {
|
|
3830
4013
|
return `[Unit]
|
|
3831
4014
|
Description=ClawMux - Smart model routing proxy
|
|
@@ -3844,7 +4027,7 @@ WantedBy=default.target
|
|
|
3844
4027
|
`;
|
|
3845
4028
|
}
|
|
3846
4029
|
function buildLaunchdPlist(bin, port, workDir) {
|
|
3847
|
-
const logDir =
|
|
4030
|
+
const logDir = import_node_path5.join(getHomeDir3(), ".openclaw", "clawmux");
|
|
3848
4031
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3849
4032
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3850
4033
|
<plist version="1.0">
|
|
@@ -3899,7 +4082,7 @@ async function installService(port, workDir) {
|
|
|
3899
4082
|
}
|
|
3900
4083
|
} else if (os === "darwin") {
|
|
3901
4084
|
await import_promises6.mkdir(LAUNCHD_DIR, { recursive: true });
|
|
3902
|
-
const logDir =
|
|
4085
|
+
const logDir = import_node_path5.join(getHomeDir3(), ".openclaw", "clawmux");
|
|
3903
4086
|
await import_promises6.mkdir(logDir, { recursive: true });
|
|
3904
4087
|
await import_promises6.writeFile(LAUNCHD_PATH, buildLaunchdPlist(bin, port, workDir));
|
|
3905
4088
|
try {
|
|
@@ -4048,8 +4231,8 @@ async function update() {
|
|
|
4048
4231
|
async function init() {
|
|
4049
4232
|
const args = process.argv.slice(2);
|
|
4050
4233
|
const noService = args.includes("--no-service");
|
|
4051
|
-
const homeDir =
|
|
4052
|
-
const openclawConfigPath = process.env.OPENCLAW_CONFIG_PATH ??
|
|
4234
|
+
const homeDir = getHomeDir3();
|
|
4235
|
+
const openclawConfigPath = process.env.OPENCLAW_CONFIG_PATH ?? import_node_path5.join(homeDir, ".openclaw", "openclaw.json");
|
|
4053
4236
|
if (!await fileExistsLocal(openclawConfigPath)) {
|
|
4054
4237
|
console.error(`[error] OpenClaw config not found at ${openclawConfigPath}`);
|
|
4055
4238
|
console.error("Set OPENCLAW_CONFIG_PATH or ensure ~/.openclaw/openclaw.json exists");
|
|
@@ -4059,8 +4242,9 @@ async function init() {
|
|
|
4059
4242
|
const backupPath = `${openclawConfigPath}.bak.${Date.now()}`;
|
|
4060
4243
|
await import_promises6.copyFile(openclawConfigPath, backupPath);
|
|
4061
4244
|
console.log(`[info] Backup created: ${backupPath}`);
|
|
4062
|
-
const clawmuxJsonPath =
|
|
4063
|
-
const examplePath =
|
|
4245
|
+
const clawmuxJsonPath = getClawmuxConfigPath();
|
|
4246
|
+
const examplePath = import_node_path5.join(process.cwd(), "clawmux.example.json");
|
|
4247
|
+
console.log(`[info] Using ClawMux config: ${clawmuxJsonPath}`);
|
|
4064
4248
|
if (!await fileExistsLocal(clawmuxJsonPath)) {
|
|
4065
4249
|
if (await fileExistsLocal(examplePath)) {
|
|
4066
4250
|
await import_promises6.copyFile(examplePath, clawmuxJsonPath);
|
|
@@ -4083,17 +4267,41 @@ async function init() {
|
|
|
4083
4267
|
if (!models.providers)
|
|
4084
4268
|
models.providers = {};
|
|
4085
4269
|
const providers = models.providers;
|
|
4270
|
+
let providerApi = PROVIDER_API_FALLBACK;
|
|
4271
|
+
try {
|
|
4272
|
+
const clawmuxRaw = await import_promises6.readFile(clawmuxJsonPath, "utf-8");
|
|
4273
|
+
const clawmuxConfig = JSON.parse(clawmuxRaw);
|
|
4274
|
+
const routing = clawmuxConfig["routing"];
|
|
4275
|
+
const routingModels = routing?.["models"];
|
|
4276
|
+
const mediumModel = routingModels?.["MEDIUM"];
|
|
4277
|
+
if (typeof mediumModel === "string" && mediumModel.length > 0) {
|
|
4278
|
+
providerApi = resolveProviderApi(mediumModel, providers);
|
|
4279
|
+
console.log(`[info] MEDIUM model: ${mediumModel} → provider api: ${providerApi}`);
|
|
4280
|
+
} else {
|
|
4281
|
+
console.log(`[info] MEDIUM model not configured yet, using default api: ${providerApi}`);
|
|
4282
|
+
}
|
|
4283
|
+
} catch {
|
|
4284
|
+
console.log(`[info] clawmux.json not readable, using default api: ${providerApi}`);
|
|
4285
|
+
}
|
|
4086
4286
|
if (providers[PROVIDER_KEY]) {
|
|
4087
|
-
|
|
4287
|
+
const existing = providers[PROVIDER_KEY];
|
|
4288
|
+
if (existing["api"] !== providerApi) {
|
|
4289
|
+
existing["api"] = providerApi;
|
|
4290
|
+
await import_promises6.writeFile(openclawConfigPath, JSON.stringify(config, null, 2) + `
|
|
4291
|
+
`);
|
|
4292
|
+
console.log(` updated ${PROVIDER_KEY} provider api → ${providerApi}`);
|
|
4293
|
+
} else {
|
|
4294
|
+
console.log(` skip ${PROVIDER_KEY} (already exists, api=${providerApi})`);
|
|
4295
|
+
}
|
|
4088
4296
|
} else {
|
|
4089
4297
|
providers[PROVIDER_KEY] = {
|
|
4090
4298
|
baseUrl: "http://localhost:3456",
|
|
4091
|
-
api:
|
|
4299
|
+
api: providerApi,
|
|
4092
4300
|
models: [{ id: "auto", name: "ClawMux Auto Router" }]
|
|
4093
4301
|
};
|
|
4094
4302
|
await import_promises6.writeFile(openclawConfigPath, JSON.stringify(config, null, 2) + `
|
|
4095
4303
|
`);
|
|
4096
|
-
console.log(` added ${PROVIDER_KEY} provider to openclaw.json`);
|
|
4304
|
+
console.log(` added ${PROVIDER_KEY} provider to openclaw.json (api=${providerApi})`);
|
|
4097
4305
|
}
|
|
4098
4306
|
const port = process.env.CLAWMUX_PORT ?? "3456";
|
|
4099
4307
|
if (!noService) {
|
|
@@ -4104,7 +4312,7 @@ async function init() {
|
|
|
4104
4312
|
[info] ClawMux setup complete!`);
|
|
4105
4313
|
console.log(`
|
|
4106
4314
|
Next steps:`);
|
|
4107
|
-
console.log(
|
|
4315
|
+
console.log(` 1. Edit ${clawmuxJsonPath} to configure your models`);
|
|
4108
4316
|
if (noService) {
|
|
4109
4317
|
console.log(" 2. Run: clawmux start");
|
|
4110
4318
|
} else {
|
|
@@ -4128,8 +4336,8 @@ async function start() {
|
|
|
4128
4336
|
}
|
|
4129
4337
|
async function uninstall() {
|
|
4130
4338
|
await removeService();
|
|
4131
|
-
const homeDir =
|
|
4132
|
-
const openclawConfigPath = process.env.OPENCLAW_CONFIG_PATH ??
|
|
4339
|
+
const homeDir = getHomeDir3();
|
|
4340
|
+
const openclawConfigPath = process.env.OPENCLAW_CONFIG_PATH ?? import_node_path5.join(homeDir, ".openclaw", "openclaw.json");
|
|
4133
4341
|
if (await fileExistsLocal(openclawConfigPath)) {
|
|
4134
4342
|
const backupPath = `${openclawConfigPath}.bak.${Date.now()}`;
|
|
4135
4343
|
await import_promises6.copyFile(openclawConfigPath, backupPath);
|
package/dist/index.cjs
CHANGED
|
@@ -447,12 +447,17 @@ function validateConfig(raw) {
|
|
|
447
447
|
}
|
|
448
448
|
|
|
449
449
|
// src/config/loader.ts
|
|
450
|
-
function
|
|
450
|
+
function getHomeDir() {
|
|
451
|
+
return process.env.HOME ?? "/root";
|
|
452
|
+
}
|
|
453
|
+
function getClawmuxConfigPath() {
|
|
451
454
|
const envPath = process.env.CLAWMUX_CONFIG;
|
|
452
|
-
if (envPath)
|
|
455
|
+
if (envPath)
|
|
453
456
|
return import_node_path.resolve(envPath);
|
|
454
|
-
|
|
455
|
-
|
|
457
|
+
return import_node_path.join(getHomeDir(), ".openclaw", "clawmux.json");
|
|
458
|
+
}
|
|
459
|
+
function findConfigPath() {
|
|
460
|
+
return getClawmuxConfigPath();
|
|
456
461
|
}
|
|
457
462
|
async function loadConfig(configPath) {
|
|
458
463
|
const filePath = configPath ?? findConfigPath();
|
|
@@ -576,7 +581,7 @@ function resolveEnvVar(value) {
|
|
|
576
581
|
}
|
|
577
582
|
return value;
|
|
578
583
|
}
|
|
579
|
-
function
|
|
584
|
+
function getHomeDir2() {
|
|
580
585
|
return process.env.HOME ?? "/root";
|
|
581
586
|
}
|
|
582
587
|
function getConfigPath(override) {
|
|
@@ -584,7 +589,7 @@ function getConfigPath(override) {
|
|
|
584
589
|
return override;
|
|
585
590
|
if (process.env.OPENCLAW_CONFIG_PATH)
|
|
586
591
|
return process.env.OPENCLAW_CONFIG_PATH;
|
|
587
|
-
return import_node_path2.join(
|
|
592
|
+
return import_node_path2.join(getHomeDir2(), ".openclaw", "openclaw.json");
|
|
588
593
|
}
|
|
589
594
|
async function readOpenClawConfig(configPath) {
|
|
590
595
|
const path = getConfigPath(configPath);
|
|
@@ -607,16 +612,9 @@ async function readOpenClawConfig(configPath) {
|
|
|
607
612
|
}
|
|
608
613
|
function getAuthProfilesPath(agentId) {
|
|
609
614
|
const id = agentId ?? "main";
|
|
610
|
-
return import_node_path2.join(
|
|
615
|
+
return import_node_path2.join(getHomeDir2(), ".openclaw", "agents", id, "agent", "auth-profiles.json");
|
|
611
616
|
}
|
|
612
|
-
|
|
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
|
-
}
|
|
617
|
+
function parseAuthProfilesFile(text) {
|
|
620
618
|
try {
|
|
621
619
|
const parsed = JSON.parse(text);
|
|
622
620
|
if (Array.isArray(parsed))
|
|
@@ -624,7 +622,7 @@ async function readAuthProfiles(agentId, profilesPath) {
|
|
|
624
622
|
if (parsed && typeof parsed === "object" && parsed.profiles) {
|
|
625
623
|
return Object.entries(parsed.profiles).map(([key, profile]) => ({
|
|
626
624
|
provider: profile.provider ?? key.split(":")[0],
|
|
627
|
-
apiKey: profile.access ?? profile.apiKey,
|
|
625
|
+
apiKey: profile.access ?? profile.apiKey ?? profile.key,
|
|
628
626
|
token: profile.token
|
|
629
627
|
})).filter((p) => {
|
|
630
628
|
const token = p.apiKey ?? p.token;
|
|
@@ -647,6 +645,33 @@ async function readAuthProfiles(agentId, profilesPath) {
|
|
|
647
645
|
throw new Error(`Failed to parse auth-profiles.json: ${message}`);
|
|
648
646
|
}
|
|
649
647
|
}
|
|
648
|
+
async function readAuthProfiles(_agentId, _profilesPath, agentsDirOverride) {
|
|
649
|
+
const agentsDir = agentsDirOverride ?? import_node_path2.join(getHomeDir2(), ".openclaw", "agents");
|
|
650
|
+
let agentDirs;
|
|
651
|
+
try {
|
|
652
|
+
agentDirs = (await import_promises4.readdir(agentsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
653
|
+
} catch {
|
|
654
|
+
const path = getAuthProfilesPath(_agentId);
|
|
655
|
+
try {
|
|
656
|
+
return parseAuthProfilesFile(await import_promises4.readFile(path, "utf-8"));
|
|
657
|
+
} catch {
|
|
658
|
+
return [];
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const ordered = ["main", ...agentDirs.filter((d) => d !== "main")];
|
|
662
|
+
const merged = new Map;
|
|
663
|
+
for (const agentId of ordered) {
|
|
664
|
+
const profilePath = import_node_path2.join(agentsDir, agentId, "agent", "auth-profiles.json");
|
|
665
|
+
try {
|
|
666
|
+
const text = await import_promises4.readFile(profilePath, "utf-8");
|
|
667
|
+
const profiles = parseAuthProfilesFile(text);
|
|
668
|
+
for (const p of profiles) {
|
|
669
|
+
merged.set(p.provider, p);
|
|
670
|
+
}
|
|
671
|
+
} catch {}
|
|
672
|
+
}
|
|
673
|
+
return Array.from(merged.values());
|
|
674
|
+
}
|
|
650
675
|
function getProviderConfig(provider, config) {
|
|
651
676
|
return config.models?.providers?.[provider];
|
|
652
677
|
}
|
|
@@ -795,6 +820,38 @@ function getAdapter(apiType) {
|
|
|
795
820
|
return adapters.get(apiType);
|
|
796
821
|
}
|
|
797
822
|
|
|
823
|
+
// src/adapters/tool-converter.ts
|
|
824
|
+
function toOpenAITools(tools) {
|
|
825
|
+
if (!Array.isArray(tools) || tools.length === 0)
|
|
826
|
+
return;
|
|
827
|
+
return tools.map((tool) => {
|
|
828
|
+
if (tool.type === "function" && tool.function)
|
|
829
|
+
return tool;
|
|
830
|
+
return {
|
|
831
|
+
type: "function",
|
|
832
|
+
function: {
|
|
833
|
+
name: tool.name ?? "",
|
|
834
|
+
description: tool.description ?? "",
|
|
835
|
+
parameters: tool.input_schema ?? tool.parameters ?? { type: "object", properties: {} }
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
function toAnthropicTools(tools) {
|
|
841
|
+
if (!Array.isArray(tools) || tools.length === 0)
|
|
842
|
+
return;
|
|
843
|
+
return tools.map((tool) => {
|
|
844
|
+
if (tool.input_schema && !tool.type)
|
|
845
|
+
return tool;
|
|
846
|
+
const fn = tool.function ?? tool;
|
|
847
|
+
return {
|
|
848
|
+
name: fn.name ?? "",
|
|
849
|
+
description: fn.description ?? "",
|
|
850
|
+
input_schema: fn.parameters ?? { type: "object", properties: {} }
|
|
851
|
+
};
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
|
|
798
855
|
// src/adapters/anthropic.ts
|
|
799
856
|
class AnthropicAdapter {
|
|
800
857
|
apiType = "anthropic-messages";
|
|
@@ -821,18 +878,42 @@ class AnthropicAdapter {
|
|
|
821
878
|
"anthropic-version": "2023-06-01",
|
|
822
879
|
"content-type": "application/json"
|
|
823
880
|
};
|
|
824
|
-
let bodyObj = {
|
|
825
|
-
...parsed.rawBody,
|
|
826
|
-
model: targetModel
|
|
827
|
-
};
|
|
828
881
|
const isHaiku = targetModel.toLowerCase().includes("haiku");
|
|
829
882
|
const hasThinking = "thinking" in parsed.rawBody;
|
|
830
883
|
if (hasThinking && !isHaiku) {
|
|
831
884
|
headers["anthropic-beta"] = "interleaved-thinking-2025-05-14";
|
|
832
885
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
886
|
+
const ANTHROPIC_SAMPLING_KEYS = [
|
|
887
|
+
"temperature",
|
|
888
|
+
"top_p",
|
|
889
|
+
"top_k",
|
|
890
|
+
"stop_sequences",
|
|
891
|
+
"metadata",
|
|
892
|
+
"service_tier"
|
|
893
|
+
];
|
|
894
|
+
const samplingParams = {};
|
|
895
|
+
for (const key of ANTHROPIC_SAMPLING_KEYS) {
|
|
896
|
+
if (key in parsed.rawBody) {
|
|
897
|
+
samplingParams[key] = parsed.rawBody[key];
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
let bodyObj = {
|
|
901
|
+
model: targetModel,
|
|
902
|
+
messages: parsed.messages,
|
|
903
|
+
stream: parsed.stream,
|
|
904
|
+
...samplingParams
|
|
905
|
+
};
|
|
906
|
+
if (parsed.system !== undefined) {
|
|
907
|
+
bodyObj.system = parsed.system;
|
|
908
|
+
}
|
|
909
|
+
if (parsed.maxTokens !== undefined) {
|
|
910
|
+
bodyObj.max_tokens = parsed.maxTokens;
|
|
911
|
+
}
|
|
912
|
+
if (parsed.rawBody.tools) {
|
|
913
|
+
bodyObj.tools = toAnthropicTools(parsed.rawBody.tools);
|
|
914
|
+
}
|
|
915
|
+
if (!isHaiku && hasThinking) {
|
|
916
|
+
bodyObj.thinking = parsed.rawBody.thinking;
|
|
836
917
|
}
|
|
837
918
|
return {
|
|
838
919
|
url,
|
|
@@ -1099,11 +1180,43 @@ class OpenAICompletionsAdapter {
|
|
|
1099
1180
|
return parseOpenAIBody(body);
|
|
1100
1181
|
}
|
|
1101
1182
|
buildUpstreamRequest(parsed, targetModel, baseUrl, auth) {
|
|
1102
|
-
const
|
|
1183
|
+
const messages = [];
|
|
1184
|
+
if (parsed.system !== undefined) {
|
|
1185
|
+
messages.push({ role: "system", content: parsed.system });
|
|
1186
|
+
}
|
|
1187
|
+
messages.push(...parsed.messages);
|
|
1188
|
+
const OPENAI_SAMPLING_KEYS = [
|
|
1189
|
+
"temperature",
|
|
1190
|
+
"top_p",
|
|
1191
|
+
"frequency_penalty",
|
|
1192
|
+
"presence_penalty",
|
|
1193
|
+
"logprobs",
|
|
1194
|
+
"top_logprobs",
|
|
1195
|
+
"seed",
|
|
1196
|
+
"stop",
|
|
1197
|
+
"n",
|
|
1198
|
+
"logit_bias",
|
|
1199
|
+
"response_format",
|
|
1200
|
+
"reasoning_effort"
|
|
1201
|
+
];
|
|
1202
|
+
const samplingParams = {};
|
|
1203
|
+
for (const key of OPENAI_SAMPLING_KEYS) {
|
|
1204
|
+
if (key in parsed.rawBody) {
|
|
1205
|
+
samplingParams[key] = parsed.rawBody[key];
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1103
1208
|
const upstreamBody = {
|
|
1104
|
-
|
|
1105
|
-
|
|
1209
|
+
model: targetModel,
|
|
1210
|
+
messages,
|
|
1211
|
+
stream: parsed.stream,
|
|
1212
|
+
...samplingParams
|
|
1106
1213
|
};
|
|
1214
|
+
if (parsed.maxTokens !== undefined) {
|
|
1215
|
+
upstreamBody.max_tokens = parsed.maxTokens;
|
|
1216
|
+
}
|
|
1217
|
+
if (parsed.rawBody.tools) {
|
|
1218
|
+
upstreamBody.tools = toOpenAITools(parsed.rawBody.tools);
|
|
1219
|
+
}
|
|
1107
1220
|
return {
|
|
1108
1221
|
url: /\/v\d+\/?$/.test(baseUrl) ? `${baseUrl.replace(/\/$/, "")}/chat/completions` : `${baseUrl}/v1/chat/completions`,
|
|
1109
1222
|
method: "POST",
|
|
@@ -1130,8 +1243,11 @@ class OpenAICompletionsAdapter {
|
|
|
1130
1243
|
if (Array.isArray(choices) && choices.length > 0) {
|
|
1131
1244
|
const choice = choices[0];
|
|
1132
1245
|
const message = choice.message;
|
|
1133
|
-
if (message
|
|
1134
|
-
|
|
1246
|
+
if (message) {
|
|
1247
|
+
const messageText = message.content ?? message.reasoning_content;
|
|
1248
|
+
if (typeof messageText === "string") {
|
|
1249
|
+
content = messageText;
|
|
1250
|
+
}
|
|
1135
1251
|
}
|
|
1136
1252
|
if (typeof choice.finish_reason === "string") {
|
|
1137
1253
|
stopReason = choice.finish_reason;
|
|
@@ -1201,16 +1317,17 @@ class OpenAICompletionsAdapter {
|
|
|
1201
1317
|
const choice = choices[0];
|
|
1202
1318
|
const delta = choice.delta;
|
|
1203
1319
|
const finishReason = choice.finish_reason;
|
|
1204
|
-
|
|
1320
|
+
const textContent = delta?.content ?? delta?.reasoning_content;
|
|
1321
|
+
if (delta?.role === "assistant" && textContent == null) {
|
|
1205
1322
|
events.push({
|
|
1206
1323
|
type: "message_start",
|
|
1207
1324
|
id: String(data.id ?? ""),
|
|
1208
1325
|
model: String(data.model ?? "")
|
|
1209
1326
|
});
|
|
1210
|
-
} else if (typeof
|
|
1327
|
+
} else if (typeof textContent === "string") {
|
|
1211
1328
|
events.push({
|
|
1212
1329
|
type: "content_delta",
|
|
1213
|
-
text:
|
|
1330
|
+
text: textContent,
|
|
1214
1331
|
index: typeof choice.index === "number" ? choice.index : 0
|
|
1215
1332
|
});
|
|
1216
1333
|
}
|
|
@@ -1242,7 +1359,7 @@ class OpenAICompletionsAdapter {
|
|
|
1242
1359
|
choices: [
|
|
1243
1360
|
{
|
|
1244
1361
|
index: 0,
|
|
1245
|
-
delta: { role: "assistant"
|
|
1362
|
+
delta: { role: "assistant" },
|
|
1246
1363
|
finish_reason: null
|
|
1247
1364
|
}
|
|
1248
1365
|
]
|
|
@@ -1302,11 +1419,40 @@ class OpenAIResponsesAdapter {
|
|
|
1302
1419
|
return parseOpenAIBody(body);
|
|
1303
1420
|
}
|
|
1304
1421
|
buildUpstreamRequest(parsed, targetModel, baseUrl, auth) {
|
|
1305
|
-
const
|
|
1422
|
+
const input = [];
|
|
1423
|
+
if (parsed.system !== undefined) {
|
|
1424
|
+
input.push({ role: "system", content: parsed.system });
|
|
1425
|
+
}
|
|
1426
|
+
input.push(...parsed.messages);
|
|
1427
|
+
const OPENAI_RESPONSES_SAMPLING_KEYS = [
|
|
1428
|
+
"temperature",
|
|
1429
|
+
"top_p",
|
|
1430
|
+
"truncation",
|
|
1431
|
+
"reasoning",
|
|
1432
|
+
"reasoning_effort",
|
|
1433
|
+
"text",
|
|
1434
|
+
"metadata",
|
|
1435
|
+
"store",
|
|
1436
|
+
"include"
|
|
1437
|
+
];
|
|
1438
|
+
const samplingParams = {};
|
|
1439
|
+
for (const key of OPENAI_RESPONSES_SAMPLING_KEYS) {
|
|
1440
|
+
if (key in parsed.rawBody) {
|
|
1441
|
+
samplingParams[key] = parsed.rawBody[key];
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1306
1444
|
const upstreamBody = {
|
|
1307
|
-
|
|
1308
|
-
|
|
1445
|
+
model: targetModel,
|
|
1446
|
+
input,
|
|
1447
|
+
stream: parsed.stream,
|
|
1448
|
+
...samplingParams
|
|
1309
1449
|
};
|
|
1450
|
+
if (parsed.maxTokens !== undefined) {
|
|
1451
|
+
upstreamBody.max_output_tokens = parsed.maxTokens;
|
|
1452
|
+
}
|
|
1453
|
+
if (parsed.rawBody.tools) {
|
|
1454
|
+
upstreamBody.tools = toOpenAITools(parsed.rawBody.tools);
|
|
1455
|
+
}
|
|
1310
1456
|
return {
|
|
1311
1457
|
url: `${baseUrl}/v1/responses`,
|
|
1312
1458
|
method: "POST",
|
|
@@ -2315,6 +2461,9 @@ class OpenAICodexAdapter {
|
|
|
2315
2461
|
}
|
|
2316
2462
|
delete upstreamBody.max_tokens;
|
|
2317
2463
|
delete upstreamBody.max_output_tokens;
|
|
2464
|
+
if (upstreamBody.tools) {
|
|
2465
|
+
upstreamBody.tools = toOpenAITools(upstreamBody.tools);
|
|
2466
|
+
}
|
|
2318
2467
|
return {
|
|
2319
2468
|
url: `${baseUrl}/codex/responses`,
|
|
2320
2469
|
method: "POST",
|
|
@@ -2971,6 +3120,7 @@ function createStreamTranslator(sourceAdapter, targetAdapter) {
|
|
|
2971
3120
|
return new TransformStream;
|
|
2972
3121
|
}
|
|
2973
3122
|
let buffer = "";
|
|
3123
|
+
let messageStarted = false;
|
|
2974
3124
|
return new TransformStream({
|
|
2975
3125
|
transform(chunk, controller) {
|
|
2976
3126
|
if (!sourceAdapter.parseStreamChunk || !targetAdapter.buildStreamChunk) {
|
|
@@ -2988,6 +3138,18 @@ function createStreamTranslator(sourceAdapter, targetAdapter) {
|
|
|
2988
3138
|
continue;
|
|
2989
3139
|
const events = sourceAdapter.parseStreamChunk(frame);
|
|
2990
3140
|
for (const event of events) {
|
|
3141
|
+
if (event.type === "message_start") {
|
|
3142
|
+
messageStarted = true;
|
|
3143
|
+
} else if (!messageStarted && (event.type === "content_delta" || event.type === "content_stop")) {
|
|
3144
|
+
messageStarted = true;
|
|
3145
|
+
const synthetic = targetAdapter.buildStreamChunk({
|
|
3146
|
+
type: "message_start",
|
|
3147
|
+
id: "",
|
|
3148
|
+
model: ""
|
|
3149
|
+
});
|
|
3150
|
+
if (synthetic)
|
|
3151
|
+
controller.enqueue(encoder.encode(synthetic));
|
|
3152
|
+
}
|
|
2991
3153
|
const translated = targetAdapter.buildStreamChunk(event);
|
|
2992
3154
|
controller.enqueue(encoder.encode(translated));
|
|
2993
3155
|
}
|
|
@@ -2997,6 +3159,18 @@ function createStreamTranslator(sourceAdapter, targetAdapter) {
|
|
|
2997
3159
|
if (buffer.trim() !== "" && sourceAdapter.parseStreamChunk && targetAdapter.buildStreamChunk) {
|
|
2998
3160
|
const events = sourceAdapter.parseStreamChunk(buffer);
|
|
2999
3161
|
for (const event of events) {
|
|
3162
|
+
if (event.type === "message_start") {
|
|
3163
|
+
messageStarted = true;
|
|
3164
|
+
} else if (!messageStarted && (event.type === "content_delta" || event.type === "content_stop")) {
|
|
3165
|
+
messageStarted = true;
|
|
3166
|
+
const synthetic = targetAdapter.buildStreamChunk({
|
|
3167
|
+
type: "message_start",
|
|
3168
|
+
id: "",
|
|
3169
|
+
model: ""
|
|
3170
|
+
});
|
|
3171
|
+
if (synthetic)
|
|
3172
|
+
controller.enqueue(encoder.encode(synthetic));
|
|
3173
|
+
}
|
|
3000
3174
|
const translated = targetAdapter.buildStreamChunk(event);
|
|
3001
3175
|
controller.enqueue(encoder.encode(translated));
|
|
3002
3176
|
}
|
|
@@ -3687,9 +3861,8 @@ function setupPipelineRoutes(config, openclawConfig, authProfiles, compressionMi
|
|
|
3687
3861
|
}
|
|
3688
3862
|
|
|
3689
3863
|
// src/index.ts
|
|
3690
|
-
var import_node_path4 = require("node:path");
|
|
3691
3864
|
async function bootstrap(portOverride) {
|
|
3692
|
-
const configPath =
|
|
3865
|
+
const configPath = getClawmuxConfigPath();
|
|
3693
3866
|
const result = await loadConfig(configPath);
|
|
3694
3867
|
if (!result.valid) {
|
|
3695
3868
|
console.error("[clawmux] Config errors:");
|