@nvae/llmswitch 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/opencode.js +28 -12
- package/dist/bridge/manager.js +45 -13
- package/dist/bridge/server.js +109 -4
- package/dist/bridge/state.js +8 -2
- package/dist/bridge/types.js +4 -2
- package/dist/commands/bridge-cmd.js +3 -2
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ import { atomicWriteFile, backupFile, ensureDir } from "../utils/fs.js";
|
|
|
6
6
|
import { applyProxyToEnvRecord, clearProxyEnvKeys } from "../utils/proxy.js";
|
|
7
7
|
import { getBackupsDir, getOpenCodeAuthPath, getOpenCodeConfigDir, getOpenCodeConfigPath, } from "../utils/paths.js";
|
|
8
8
|
import { setActiveProfile } from "../store/profiles.js";
|
|
9
|
+
import { ensureBridgeForProfile, profileNeedsBridge, } from "../bridge/manager.js";
|
|
9
10
|
function providerId(name) {
|
|
10
11
|
return `llms-${name}`.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
11
12
|
}
|
|
@@ -30,7 +31,7 @@ export function readOpenCodeAuth(path = getOpenCodeAuthPath()) {
|
|
|
30
31
|
return {};
|
|
31
32
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
32
33
|
}
|
|
33
|
-
export function buildOpenCodeProviderBlock(profile) {
|
|
34
|
+
export function buildOpenCodeProviderBlock(profile, overrides) {
|
|
34
35
|
const models = {};
|
|
35
36
|
for (const id of profile.models.list) {
|
|
36
37
|
models[id] = { name: id };
|
|
@@ -39,10 +40,12 @@ export function buildOpenCodeProviderBlock(profile) {
|
|
|
39
40
|
models[profile.models.default] = { name: profile.models.default };
|
|
40
41
|
}
|
|
41
42
|
const options = {
|
|
42
|
-
baseURL:
|
|
43
|
+
baseURL: overrides?.baseURL ||
|
|
44
|
+
normalizeBaseUrlForFormat(profile.apiFormat, profile.baseUrl),
|
|
43
45
|
};
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
const apiKey = overrides?.apiKey ?? profile.apiKey;
|
|
47
|
+
if (apiKey) {
|
|
48
|
+
options.apiKey = apiKey;
|
|
46
49
|
}
|
|
47
50
|
if (profile.headers && Object.keys(profile.headers).length > 0) {
|
|
48
51
|
options.headers = { ...profile.headers };
|
|
@@ -54,19 +57,23 @@ export function buildOpenCodeProviderBlock(profile) {
|
|
|
54
57
|
models,
|
|
55
58
|
};
|
|
56
59
|
}
|
|
57
|
-
export function buildOpenCodeConfig(existing, profile) {
|
|
60
|
+
export function buildOpenCodeConfig(existing, profile, overrides) {
|
|
58
61
|
assertCompatible("opencode", profile.apiFormat);
|
|
59
62
|
const id = providerId(profile.name);
|
|
60
63
|
const providers = {
|
|
61
64
|
...(existing.provider || {}),
|
|
62
65
|
};
|
|
63
|
-
providers[id] = buildOpenCodeProviderBlock(profile);
|
|
64
|
-
// Optional top-level env for proxy (OpenCode may pass through)
|
|
66
|
+
providers[id] = buildOpenCodeProviderBlock(profile, overrides);
|
|
67
|
+
// Optional top-level env for proxy (OpenCode may pass through). When the
|
|
68
|
+
// profile routes through the bridge, the upstream proxy is applied inside the
|
|
69
|
+
// bridge; skip env-var proxy injection so OpenCode doesn't apply its own.
|
|
65
70
|
const env = {
|
|
66
71
|
...(existing.env || {}),
|
|
67
72
|
};
|
|
68
73
|
clearProxyEnvKeys(env);
|
|
69
|
-
|
|
74
|
+
if (!overrides) {
|
|
75
|
+
applyProxyToEnvRecord(env, profile.proxy);
|
|
76
|
+
}
|
|
70
77
|
const next = {
|
|
71
78
|
...existing,
|
|
72
79
|
$schema: existing.$schema || "https://opencode.ai/config.json",
|
|
@@ -92,18 +99,25 @@ export function buildOpenCodeAuth(existing, profile) {
|
|
|
92
99
|
}
|
|
93
100
|
return next;
|
|
94
101
|
}
|
|
95
|
-
export function applyOpenCodeProfile(profile) {
|
|
102
|
+
export async function applyOpenCodeProfile(profile) {
|
|
96
103
|
assertCompatible("opencode", profile.apiFormat);
|
|
97
104
|
ensureDir(getOpenCodeConfigDir());
|
|
98
105
|
ensureDir(dirname(getOpenCodeAuthPath()));
|
|
106
|
+
let bridgeConnection = null;
|
|
107
|
+
if (profileNeedsBridge(profile)) {
|
|
108
|
+
bridgeConnection = await ensureBridgeForProfile(profile, "opencode");
|
|
109
|
+
}
|
|
99
110
|
const configPath = getOpenCodeConfigPath();
|
|
100
111
|
const authPath = getOpenCodeAuthPath();
|
|
101
112
|
const existing = readOpenCodeConfig(configPath);
|
|
102
113
|
const backupPath = backupFile(configPath, getBackupsDir("opencode"), "opencode");
|
|
103
114
|
backupFile(authPath, getBackupsDir("opencode"), "auth");
|
|
104
|
-
const nextConfig = buildOpenCodeConfig(existing, profile
|
|
115
|
+
const nextConfig = buildOpenCodeConfig(existing, profile, bridgeConnection
|
|
116
|
+
? { baseURL: bridgeConnection.baseUrl, apiKey: bridgeConnection.clientToken }
|
|
117
|
+
: undefined);
|
|
105
118
|
atomicWriteFile(configPath, JSON.stringify(nextConfig, null, 2) + "\n");
|
|
106
|
-
const
|
|
119
|
+
const bridgeApiKey = bridgeConnection?.clientToken || profile.apiKey;
|
|
120
|
+
const nextAuth = buildOpenCodeAuth(readOpenCodeAuth(authPath), { ...profile, apiKey: bridgeApiKey });
|
|
107
121
|
atomicWriteFile(authPath, JSON.stringify(nextAuth, null, 2) + "\n");
|
|
108
122
|
setActiveProfile("opencode", profile.name);
|
|
109
123
|
return {
|
|
@@ -111,7 +125,9 @@ export function applyOpenCodeProfile(profile) {
|
|
|
111
125
|
profile: profile.name,
|
|
112
126
|
configPath,
|
|
113
127
|
backupPath,
|
|
114
|
-
restartHint:
|
|
128
|
+
restartHint: bridgeConnection
|
|
129
|
+
? "已通过本地 bridge 启用供应商(上游代理在 bridge 内生效)。请重新启动 OpenCode 会话使配置生效。"
|
|
130
|
+
: "请重新启动 OpenCode 会话以使配置与代理生效。",
|
|
115
131
|
};
|
|
116
132
|
}
|
|
117
133
|
export function deactivateOpenCodeProfile(profileName) {
|
package/dist/bridge/manager.js
CHANGED
|
@@ -28,8 +28,14 @@ export class BridgeControlError extends Error {
|
|
|
28
28
|
export function profileNeedsBridge(profile) {
|
|
29
29
|
return profile.apiFormat === "openai-chat";
|
|
30
30
|
}
|
|
31
|
+
function isBunRuntime() {
|
|
32
|
+
return typeof process.versions?.bun ===
|
|
33
|
+
"string";
|
|
34
|
+
}
|
|
31
35
|
export function bridgeToolForCliTool(tool) {
|
|
32
|
-
return tool === "codex" || tool === "claude"
|
|
36
|
+
return tool === "codex" || tool === "claude" || tool === "opencode"
|
|
37
|
+
? tool
|
|
38
|
+
: null;
|
|
33
39
|
}
|
|
34
40
|
export function upstreamFromProfile(profile, tool, clientToken = null) {
|
|
35
41
|
const mode = tool === "claude" ? "chat" : profile.bridgeMode || "chat";
|
|
@@ -186,17 +192,26 @@ export async function startBridgeDaemon(host = DEFAULT_BRIDGE_HOST, port = DEFAU
|
|
|
186
192
|
instance: null,
|
|
187
193
|
pid: null,
|
|
188
194
|
}));
|
|
189
|
-
const
|
|
190
|
-
const args = [
|
|
195
|
+
const runner = resolveBridgeDaemonRunner();
|
|
196
|
+
const args = [
|
|
197
|
+
runner.entry,
|
|
198
|
+
"bridge",
|
|
199
|
+
"serve",
|
|
200
|
+
"--host",
|
|
201
|
+
host,
|
|
202
|
+
"--port",
|
|
203
|
+
String(port),
|
|
204
|
+
];
|
|
191
205
|
if (allowRemote)
|
|
192
206
|
args.push("--allow-remote");
|
|
193
|
-
const child = spawn(
|
|
207
|
+
const child = spawn(runner.command, args, {
|
|
194
208
|
detached: true,
|
|
195
209
|
stdio: "ignore",
|
|
196
210
|
env: {
|
|
197
211
|
...process.env,
|
|
198
212
|
LLM_SWITCH_BRIDGE_PORT: String(port),
|
|
199
213
|
LLM_SWITCH_BRIDGE_HOST: host,
|
|
214
|
+
LLM_SWITCH_BRIDGE_RUNTIME: runner.command,
|
|
200
215
|
},
|
|
201
216
|
});
|
|
202
217
|
child.unref();
|
|
@@ -243,6 +258,9 @@ export async function forceStopBridge() {
|
|
|
243
258
|
await stopBridge();
|
|
244
259
|
}
|
|
245
260
|
export async function runBridgeForeground(host, port, allowRemote = false) {
|
|
261
|
+
if (process.env.LLM_SWITCH_BRIDGE_RUNTIME !== "node" && isBunRuntime()) {
|
|
262
|
+
console.error("注意:bridge 当前由 Bun 运行,Bun 会本地解析 DNS,可能导致 socks5h 代理出口地区错误。建议用「node dist/index.js bridge serve」运行。");
|
|
263
|
+
}
|
|
246
264
|
assertBridgeListenerAllowed(host, allowRemote);
|
|
247
265
|
const listener = resolveBridgeListener({ host, port, allowRemote });
|
|
248
266
|
const previous = readBridgeState();
|
|
@@ -321,15 +339,29 @@ export async function runBridgeForeground(host, port, allowRemote = false) {
|
|
|
321
339
|
process.removeListener("SIGINT", onSignal);
|
|
322
340
|
process.removeListener("SIGTERM", onSignal);
|
|
323
341
|
}
|
|
324
|
-
|
|
342
|
+
/**
|
|
343
|
+
* Resolve how the detached bridge daemon is launched. The bridge must run under
|
|
344
|
+
* Node.js: Bun's node:https stack resolves target DNS locally, which defeats
|
|
345
|
+
* socks5h remote-DNS and can produce wrong-region egress. Node delegates the
|
|
346
|
+
* hostname to the socks agent, giving correct remote-DNS behavior.
|
|
347
|
+
*/
|
|
348
|
+
function resolveBridgeDaemonRunner() {
|
|
349
|
+
const candidates = [
|
|
350
|
+
fileURLToPath(new URL("../index.js", import.meta.url)),
|
|
351
|
+
fileURLToPath(new URL("../../dist/index.js", import.meta.url)),
|
|
352
|
+
fileURLToPath(new URL("../index.ts", import.meta.url)),
|
|
353
|
+
];
|
|
354
|
+
for (const candidate of candidates) {
|
|
355
|
+
if (existsSync(candidate)) {
|
|
356
|
+
if (candidate.endsWith(".js")) {
|
|
357
|
+
return { command: "node", entry: candidate };
|
|
358
|
+
}
|
|
359
|
+
return { command: process.execPath, entry: candidate };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
325
362
|
const running = process.argv[1];
|
|
326
|
-
if (running && existsSync(running))
|
|
327
|
-
return running;
|
|
328
|
-
|
|
329
|
-
if (existsSync(compiled))
|
|
330
|
-
return compiled;
|
|
331
|
-
const source = fileURLToPath(new URL("../index.ts", import.meta.url));
|
|
332
|
-
if (existsSync(source))
|
|
333
|
-
return source;
|
|
363
|
+
if (running && existsSync(running)) {
|
|
364
|
+
return { command: process.execPath, entry: running };
|
|
365
|
+
}
|
|
334
366
|
throw new BridgeControlError("无法定位 llmswitch 入口文件");
|
|
335
367
|
}
|
package/dist/bridge/server.js
CHANGED
|
@@ -29,7 +29,7 @@ function authenticateDataRequest(req, upstream, tool) {
|
|
|
29
29
|
if (!upstream?.clientToken || upstream.migrationRequired)
|
|
30
30
|
return false;
|
|
31
31
|
const bearer = bearerToken(req);
|
|
32
|
-
if (tool === "codex") {
|
|
32
|
+
if (tool === "codex" || tool === "opencode") {
|
|
33
33
|
return constantTimeTokenEqual(upstream.clientToken, bearer);
|
|
34
34
|
}
|
|
35
35
|
const apiKey = headerValue(req.headers["x-api-key"]);
|
|
@@ -40,7 +40,8 @@ function authenticateDataRequest(req, upstream, tool) {
|
|
|
40
40
|
}
|
|
41
41
|
function authenticateModelsRequest(req, upstreams) {
|
|
42
42
|
return (authenticateDataRequest(req, upstreams.codex, "codex") ||
|
|
43
|
-
authenticateDataRequest(req, upstreams.claude, "claude")
|
|
43
|
+
authenticateDataRequest(req, upstreams.claude, "claude") ||
|
|
44
|
+
authenticateDataRequest(req, upstreams.opencode, "opencode"));
|
|
44
45
|
}
|
|
45
46
|
function readBody(req, maxBytes = parseBridgeRuntimeLimits().maxBodyBytes) {
|
|
46
47
|
return new Promise((resolve, reject) => {
|
|
@@ -156,7 +157,7 @@ async function fetchModelsJson(upstream) {
|
|
|
156
157
|
}
|
|
157
158
|
}
|
|
158
159
|
async function proxyModelsMerged(_req, res, upstreams) {
|
|
159
|
-
const sides = [upstreams.codex, upstreams.claude].filter((u) => Boolean(u?.baseUrl));
|
|
160
|
+
const sides = [upstreams.codex, upstreams.claude, upstreams.opencode].filter((u) => Boolean(u?.baseUrl));
|
|
160
161
|
if (!sides.length) {
|
|
161
162
|
sendJson(res, 503, {
|
|
162
163
|
error: { message: "Bridge 未配置上游" },
|
|
@@ -293,6 +294,52 @@ async function forwardChatResponses(req, res, upstream, body, wantStream) {
|
|
|
293
294
|
}
|
|
294
295
|
await pipeChatStreamToResponses(response, res, String(body.model || ""), customTools, true);
|
|
295
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* OpenCode-facing passthrough: forward an OpenAI chat request verbatim to the
|
|
299
|
+
* upstream `/chat/completions` (with llm-switch transport applying the proxy)
|
|
300
|
+
* and relay the raw response, preserving streaming for SSE.
|
|
301
|
+
*/
|
|
302
|
+
async function forwardOpenCodeChat(_req, res, upstream, bodyBuf) {
|
|
303
|
+
let body;
|
|
304
|
+
try {
|
|
305
|
+
body = JSON.parse(bodyBuf.toString("utf8"));
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
sendJson(res, 400, { error: { message: "Invalid JSON body" } });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const wantStream = Boolean(body.stream);
|
|
312
|
+
const url = joinUrl(upstream.baseUrl, "/chat/completions");
|
|
313
|
+
let response;
|
|
314
|
+
try {
|
|
315
|
+
response = await requestUpstream(upstream, url, "POST", bodyBuf.toString("utf8"));
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
sendJson(res, 502, {
|
|
319
|
+
error: {
|
|
320
|
+
message: `Upstream chat 请求失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (!response.ok) {
|
|
326
|
+
const text = await response.text();
|
|
327
|
+
res.writeHead(response.status, {
|
|
328
|
+
"Content-Type": response.headers.get("content-type") || "application/json",
|
|
329
|
+
});
|
|
330
|
+
res.end(text);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (!wantStream) {
|
|
334
|
+
const text = await response.text();
|
|
335
|
+
res.writeHead(response.status, {
|
|
336
|
+
"Content-Type": response.headers.get("content-type") || "application/json",
|
|
337
|
+
});
|
|
338
|
+
res.end(text);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
await pipeRawStream(response, res);
|
|
342
|
+
}
|
|
296
343
|
async function forwardCompletions(_req, res, upstream, body, wantStream) {
|
|
297
344
|
const completionReq = responsesToCompletionsRequest(body);
|
|
298
345
|
const customTools = collectCustomToolNames(body.tools);
|
|
@@ -444,6 +491,34 @@ async function pipeChatStreamToAnthropic(upstream, res, model) {
|
|
|
444
491
|
res.end();
|
|
445
492
|
}
|
|
446
493
|
}
|
|
494
|
+
/** Relay an upstream SSE stream verbatim (OpenCode chat passthrough). */
|
|
495
|
+
async function pipeRawStream(upstream, res) {
|
|
496
|
+
res.writeHead(200, {
|
|
497
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
498
|
+
"Cache-Control": "no-cache, no-transform",
|
|
499
|
+
Connection: "keep-alive",
|
|
500
|
+
"X-Accel-Buffering": "no",
|
|
501
|
+
});
|
|
502
|
+
const reader = upstream.body?.getReader();
|
|
503
|
+
if (!reader) {
|
|
504
|
+
res.end();
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
while (true) {
|
|
509
|
+
const { done, value } = await reader.read();
|
|
510
|
+
if (done)
|
|
511
|
+
break;
|
|
512
|
+
res.write(value);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
// Connection dropped; best-effort close.
|
|
517
|
+
}
|
|
518
|
+
finally {
|
|
519
|
+
res.end();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
447
522
|
export function createBridgeServer(options = {}) {
|
|
448
523
|
return createServer(async (req, res) => {
|
|
449
524
|
try {
|
|
@@ -487,6 +562,13 @@ export function createBridgeServer(options = {}) {
|
|
|
487
562
|
migrationRequired: merged.claude.migrationRequired === true,
|
|
488
563
|
}
|
|
489
564
|
: null,
|
|
565
|
+
opencode: merged.opencode
|
|
566
|
+
? {
|
|
567
|
+
mode: merged.opencode.mode,
|
|
568
|
+
profile: merged.opencode.profileName || null,
|
|
569
|
+
migrationRequired: merged.opencode.migrationRequired === true,
|
|
570
|
+
}
|
|
571
|
+
: null,
|
|
490
572
|
},
|
|
491
573
|
});
|
|
492
574
|
return;
|
|
@@ -584,9 +666,32 @@ export function createBridgeServer(options = {}) {
|
|
|
584
666
|
await handleMessages(req, res, merged.claude, body);
|
|
585
667
|
return;
|
|
586
668
|
}
|
|
669
|
+
if (req.method === "POST" &&
|
|
670
|
+
(path === "/v1/chat/completions" || path === "/chat/completions")) {
|
|
671
|
+
if (!merged.opencode?.baseUrl) {
|
|
672
|
+
sendJson(res, 503, {
|
|
673
|
+
error: {
|
|
674
|
+
message: "Bridge 未配置 OpenCode 上游。请先 llms opencode use <openai-chat profile>",
|
|
675
|
+
},
|
|
676
|
+
});
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (!authenticateDataRequest(req, merged.opencode, "opencode")) {
|
|
680
|
+
sendJson(res, 401, {
|
|
681
|
+
error: {
|
|
682
|
+
code: "invalid_bridge_token",
|
|
683
|
+
message: "Bridge token 无效;请重新执行 llms opencode use <profile>",
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const body = await readBody(req);
|
|
689
|
+
await forwardOpenCodeChat(req, res, merged.opencode, body);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
587
692
|
sendJson(res, 404, {
|
|
588
693
|
error: {
|
|
589
|
-
message: `Bridge 支持 GET /v1/models、POST /v1/responses、POST /v1/messages(当前: ${req.method} ${path})`,
|
|
694
|
+
message: `Bridge 支持 GET /v1/models、POST /v1/responses、POST /v1/messages、POST /v1/chat/completions(当前: ${req.method} ${path})`,
|
|
590
695
|
},
|
|
591
696
|
});
|
|
592
697
|
}
|
package/dist/bridge/state.js
CHANGED
|
@@ -63,7 +63,10 @@ function isLegacyUpstream(raw) {
|
|
|
63
63
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
64
64
|
return false;
|
|
65
65
|
const row = raw;
|
|
66
|
-
return typeof row.baseUrl === "string" &&
|
|
66
|
+
return (typeof row.baseUrl === "string" &&
|
|
67
|
+
!("codex" in row) &&
|
|
68
|
+
!("claude" in row) &&
|
|
69
|
+
!("opencode" in row));
|
|
67
70
|
}
|
|
68
71
|
export function normalizeBridgeUpstreams(raw) {
|
|
69
72
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
@@ -71,11 +74,12 @@ export function normalizeBridgeUpstreams(raw) {
|
|
|
71
74
|
}
|
|
72
75
|
const row = raw;
|
|
73
76
|
if (isLegacyUpstream(raw)) {
|
|
74
|
-
return { codex: raw, claude: null };
|
|
77
|
+
return { codex: raw, claude: null, opencode: null };
|
|
75
78
|
}
|
|
76
79
|
return {
|
|
77
80
|
codex: row.codex ?? null,
|
|
78
81
|
claude: row.claude ?? null,
|
|
82
|
+
opencode: row.opencode ?? null,
|
|
79
83
|
};
|
|
80
84
|
}
|
|
81
85
|
/** Legacy upstreams cannot authenticate until reapplied. */
|
|
@@ -155,6 +159,7 @@ function readLegacyMigrationState() {
|
|
|
155
159
|
upstreams: {
|
|
156
160
|
codex: markUpstreamMigrationRequired(upstreams.codex),
|
|
157
161
|
claude: markUpstreamMigrationRequired(upstreams.claude),
|
|
162
|
+
opencode: markUpstreamMigrationRequired(upstreams.opencode),
|
|
158
163
|
},
|
|
159
164
|
pending: null,
|
|
160
165
|
});
|
|
@@ -224,6 +229,7 @@ function persistState(next) {
|
|
|
224
229
|
upstreams: {
|
|
225
230
|
codex: next.upstreams.codex,
|
|
226
231
|
claude: next.upstreams.claude,
|
|
232
|
+
opencode: next.upstreams.opencode,
|
|
227
233
|
},
|
|
228
234
|
pending: next.pending,
|
|
229
235
|
};
|
package/dist/bridge/types.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export const DEFAULT_BRIDGE_PORT = 17890;
|
|
2
2
|
export const DEFAULT_BRIDGE_HOST = "127.0.0.1";
|
|
3
3
|
export function emptyUpstreams() {
|
|
4
|
-
return { codex: null, claude: null };
|
|
4
|
+
return { codex: null, claude: null, opencode: null };
|
|
5
5
|
}
|
|
6
6
|
export function hasAnyUpstream(upstreams) {
|
|
7
|
-
return Boolean(upstreams.codex?.baseUrl ||
|
|
7
|
+
return Boolean(upstreams.codex?.baseUrl ||
|
|
8
|
+
upstreams.claude?.baseUrl ||
|
|
9
|
+
upstreams.opencode?.baseUrl);
|
|
8
10
|
}
|
|
@@ -72,6 +72,7 @@ export function registerBridgeCommand(program) {
|
|
|
72
72
|
upstreams: {
|
|
73
73
|
codex: summarize(state.upstreams.codex),
|
|
74
74
|
claude: summarize(state.upstreams.claude),
|
|
75
|
+
opencode: summarize(state.upstreams.opencode),
|
|
75
76
|
},
|
|
76
77
|
};
|
|
77
78
|
if (opts.json) {
|
|
@@ -80,9 +81,9 @@ export function registerBridgeCommand(program) {
|
|
|
80
81
|
}
|
|
81
82
|
console.log(`状态:${alive ? "运行中" : "未运行"}`);
|
|
82
83
|
console.log(`根地址:${data.rootUrl}`);
|
|
83
|
-
console.log(`Codex base:${data.codexBaseUrl}`);
|
|
84
|
+
console.log(`Codex/OpenCode base:${data.codexBaseUrl}`);
|
|
84
85
|
console.log(`PID:${pid ?? "-"}`);
|
|
85
|
-
for (const tool of ["codex", "claude"]) {
|
|
86
|
+
for (const tool of ["codex", "claude", "opencode"]) {
|
|
86
87
|
const u = data.upstreams[tool];
|
|
87
88
|
if (u) {
|
|
88
89
|
console.log(`${tool} 上游:${u.baseUrl}(${u.mode}) profile=${u.profile ?? "-"}`);
|