@nvae/llmswitch 0.2.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/LICENSE +21 -0
- package/README.md +303 -0
- package/dist/adapters/claude.js +117 -0
- package/dist/adapters/codex.js +229 -0
- package/dist/adapters/index.js +33 -0
- package/dist/adapters/merge.js +21 -0
- package/dist/adapters/opencode.js +162 -0
- package/dist/bridge/anthropic-translate-request.js +226 -0
- package/dist/bridge/anthropic-translate-response.js +265 -0
- package/dist/bridge/manager.js +240 -0
- package/dist/bridge/server.js +487 -0
- package/dist/bridge/state.js +125 -0
- package/dist/bridge/translate-request.js +385 -0
- package/dist/bridge/translate-response.js +509 -0
- package/dist/bridge/types.js +8 -0
- package/dist/cli.js +48 -0
- package/dist/commands/bridge-cmd.js +113 -0
- package/dist/commands/launch-cmd.js +83 -0
- package/dist/commands/launch.js +175 -0
- package/dist/commands/prompts.js +595 -0
- package/dist/commands/tool.js +380 -0
- package/dist/formats/compatibility.js +33 -0
- package/dist/index.js +3 -0
- package/dist/presets/index.js +40 -0
- package/dist/store/profiles.js +202 -0
- package/dist/types.js +17 -0
- package/dist/utils/base-url.js +40 -0
- package/dist/utils/fetch-models.js +177 -0
- package/dist/utils/fs.js +40 -0
- package/dist/utils/paths.js +67 -0
- package/dist/utils/proxy.js +68 -0
- package/package.json +49 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { isTool, TOOLS } from "../types.js";
|
|
2
|
+
import { launchTool, resolveBinary, resolveLaunchTarget, } from "./launch.js";
|
|
3
|
+
/**
|
|
4
|
+
* ollama-like quick start:
|
|
5
|
+
* llms launch codex --model gpt-4.1
|
|
6
|
+
* llms launch codex gpt-4.1
|
|
7
|
+
* llms run claude --profile my-provider
|
|
8
|
+
* llms launch opencode my-model -- resume
|
|
9
|
+
*/
|
|
10
|
+
export function registerLaunchCommand(program) {
|
|
11
|
+
program
|
|
12
|
+
.command("launch")
|
|
13
|
+
.alias("run")
|
|
14
|
+
.description("切换模型并启动对应 CLI(类似 ollama run)。例:llms launch codex --model gpt-4.1")
|
|
15
|
+
.argument("<tool>", `目标工具:${TOOLS.join(" | ")}`)
|
|
16
|
+
.argument("[parts...]", "可选:模型 ID,以及传给底层 CLI 的参数")
|
|
17
|
+
.option("-m, --model <id>", "要启用的模型 ID")
|
|
18
|
+
.option("-p, --profile <name>", "指定 profile(默认:包含该模型的 profile / 当前启用)")
|
|
19
|
+
.option("--print-only", "只写入配置,不启动 CLI")
|
|
20
|
+
.option("--dry-run", "只打印计划,不写配置、不启动")
|
|
21
|
+
.option("--json", "JSON 输出")
|
|
22
|
+
.action(async (toolArg, parts = [], opts) => {
|
|
23
|
+
if (!isTool(toolArg)) {
|
|
24
|
+
throw new Error(`未知工具「${toolArg}」。可选:${TOOLS.join(", ")}`);
|
|
25
|
+
}
|
|
26
|
+
let model = opts.model?.trim() || undefined;
|
|
27
|
+
const queue = [...parts];
|
|
28
|
+
if (!model && queue[0] && !queue[0].startsWith("-")) {
|
|
29
|
+
model = queue.shift();
|
|
30
|
+
}
|
|
31
|
+
const passthrough = queue;
|
|
32
|
+
if (opts.dryRun) {
|
|
33
|
+
const target = resolveLaunchTarget(toolArg, {
|
|
34
|
+
model,
|
|
35
|
+
profile: opts.profile,
|
|
36
|
+
});
|
|
37
|
+
const plan = {
|
|
38
|
+
tool: toolArg,
|
|
39
|
+
profile: target.profile.name,
|
|
40
|
+
model: target.model,
|
|
41
|
+
binary: resolveBinary(toolArg),
|
|
42
|
+
args: passthrough,
|
|
43
|
+
dryRun: true,
|
|
44
|
+
};
|
|
45
|
+
if (opts.json) {
|
|
46
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const argStr = passthrough.length
|
|
50
|
+
? ` ${passthrough.join(" ")}`
|
|
51
|
+
: "";
|
|
52
|
+
console.log(`[dry-run] 启用 ${toolArg}/${plan.profile} → ${plan.model},然后执行:${plan.binary}${argStr}`);
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const plan = await launchTool({
|
|
57
|
+
tool: toolArg,
|
|
58
|
+
model,
|
|
59
|
+
profile: opts.profile,
|
|
60
|
+
args: passthrough,
|
|
61
|
+
printOnly: opts.printOnly,
|
|
62
|
+
});
|
|
63
|
+
if (opts.json) {
|
|
64
|
+
console.log(JSON.stringify({
|
|
65
|
+
tool: plan.tool,
|
|
66
|
+
profile: plan.profile.name,
|
|
67
|
+
model: plan.model,
|
|
68
|
+
binary: plan.binary,
|
|
69
|
+
args: plan.args,
|
|
70
|
+
configPath: plan.configPath,
|
|
71
|
+
applied: plan.applied,
|
|
72
|
+
printOnly: Boolean(opts.printOnly),
|
|
73
|
+
}, null, 2));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// When actually spawning, launchTool already printed status before exec.
|
|
77
|
+
if (opts.printOnly) {
|
|
78
|
+
console.error(`已切换 ${plan.tool}/${plan.profile.name} → ${plan.model}`);
|
|
79
|
+
console.error(plan.restartHint);
|
|
80
|
+
console.error(`未启动 CLI(--print-only)。手动运行:${plan.binary}`);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { delimiter, join } from "node:path";
|
|
4
|
+
import { isTool } from "../types.js";
|
|
5
|
+
import { applyProfile } from "../adapters/index.js";
|
|
6
|
+
import { getActiveProfile, getDefaultProfile, listProfiles, requireProfile, saveProfile, ensureDefaultProvider, } from "../store/profiles.js";
|
|
7
|
+
const BINARY = {
|
|
8
|
+
claude: "claude",
|
|
9
|
+
codex: "codex",
|
|
10
|
+
opencode: "opencode",
|
|
11
|
+
};
|
|
12
|
+
const BINARY_ENV = {
|
|
13
|
+
claude: "CLAUDE_BIN",
|
|
14
|
+
codex: "CODEX_BIN",
|
|
15
|
+
opencode: "OPENCODE_BIN",
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Resolve profile/model, write tool config, optionally spawn the native CLI.
|
|
19
|
+
*/
|
|
20
|
+
export async function launchTool(options) {
|
|
21
|
+
if (!isTool(options.tool)) {
|
|
22
|
+
throw new Error(`未知工具「${options.tool}」。可选:claude、codex、opencode`);
|
|
23
|
+
}
|
|
24
|
+
const tool = options.tool;
|
|
25
|
+
let { profile, model } = resolveLaunchTarget(tool, {
|
|
26
|
+
model: options.model,
|
|
27
|
+
profile: options.profile,
|
|
28
|
+
});
|
|
29
|
+
if (profile.models.default !== model) {
|
|
30
|
+
if (!profile.models.list.includes(model)) {
|
|
31
|
+
profile.models.list.push(model);
|
|
32
|
+
}
|
|
33
|
+
profile.models.default = model;
|
|
34
|
+
saveProfile(tool, profile);
|
|
35
|
+
profile = requireProfile(tool, profile.name);
|
|
36
|
+
}
|
|
37
|
+
else if (!profile.models.list.includes(model)) {
|
|
38
|
+
profile.models.list.push(model);
|
|
39
|
+
saveProfile(tool, profile);
|
|
40
|
+
profile = requireProfile(tool, profile.name);
|
|
41
|
+
}
|
|
42
|
+
const result = await applyProfile(tool, profile);
|
|
43
|
+
const binary = resolveBinary(tool);
|
|
44
|
+
const args = options.args ?? [];
|
|
45
|
+
const plan = {
|
|
46
|
+
tool,
|
|
47
|
+
profile,
|
|
48
|
+
model,
|
|
49
|
+
binary,
|
|
50
|
+
args,
|
|
51
|
+
applied: true,
|
|
52
|
+
configPath: result.configPath,
|
|
53
|
+
restartHint: result.restartHint,
|
|
54
|
+
};
|
|
55
|
+
if (options.dryRun || options.printOnly) {
|
|
56
|
+
return plan;
|
|
57
|
+
}
|
|
58
|
+
const binPath = which(binary);
|
|
59
|
+
if (!binPath) {
|
|
60
|
+
throw new Error(`未找到可执行文件「${binary}」。请确认已安装,或设置环境变量 ${BINARY_ENV[tool]}=/path/to/${binary}`);
|
|
61
|
+
}
|
|
62
|
+
// Announce before handing over the TTY
|
|
63
|
+
console.error(`已切换 ${tool}/${profile.name} → ${model}`);
|
|
64
|
+
console.error(`启动 ${binary}${args.length ? ` ${args.join(" ")}` : ""}`);
|
|
65
|
+
const code = await spawnInherited(binPath, args);
|
|
66
|
+
process.exitCode = code ?? 1;
|
|
67
|
+
return plan;
|
|
68
|
+
}
|
|
69
|
+
export function resolveLaunchTarget(tool, opts) {
|
|
70
|
+
const profiles = listProfiles(tool);
|
|
71
|
+
if (profiles.length === 0) {
|
|
72
|
+
throw new Error(`暂无 ${tool} 供应商,请先:llms ${tool} provider`);
|
|
73
|
+
}
|
|
74
|
+
ensureDefaultProvider(tool);
|
|
75
|
+
const modelQuery = opts.model?.trim() || undefined;
|
|
76
|
+
let profile = null;
|
|
77
|
+
if (opts.profile) {
|
|
78
|
+
profile = requireProfile(tool, opts.profile);
|
|
79
|
+
}
|
|
80
|
+
if (!profile && modelQuery) {
|
|
81
|
+
profile = findProfileForModel(profiles, modelQuery);
|
|
82
|
+
}
|
|
83
|
+
if (!profile) {
|
|
84
|
+
profile = getActiveProfile(tool);
|
|
85
|
+
}
|
|
86
|
+
if (!profile) {
|
|
87
|
+
profile = getDefaultProfile(tool);
|
|
88
|
+
}
|
|
89
|
+
if (!profile) {
|
|
90
|
+
throw new Error(`未指定供应商。请使用 --profile <name>,或先:llms ${tool} provider`);
|
|
91
|
+
}
|
|
92
|
+
let model = profile.models.default;
|
|
93
|
+
if (modelQuery) {
|
|
94
|
+
const inSelected = matchModel(profile.models.list, modelQuery);
|
|
95
|
+
if (inSelected) {
|
|
96
|
+
model = inSelected;
|
|
97
|
+
}
|
|
98
|
+
else if (!opts.profile) {
|
|
99
|
+
const other = findProfileForModel(profiles, modelQuery);
|
|
100
|
+
if (other) {
|
|
101
|
+
profile = other;
|
|
102
|
+
model = matchModel(other.models.list, modelQuery) || modelQuery;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
model = modelQuery;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
model = modelQuery;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { profile, model };
|
|
113
|
+
}
|
|
114
|
+
export function findProfileForModel(profiles, query) {
|
|
115
|
+
for (const profile of profiles) {
|
|
116
|
+
if (matchModel(profile.models.list, query))
|
|
117
|
+
return profile;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
export function matchModel(list, query) {
|
|
122
|
+
if (list.includes(query))
|
|
123
|
+
return query;
|
|
124
|
+
const ci = list.find((m) => m.toLowerCase() === query.toLowerCase());
|
|
125
|
+
if (ci)
|
|
126
|
+
return ci;
|
|
127
|
+
const fuzzy = list.filter((m) => modelFuzzyEqual(m, query));
|
|
128
|
+
if (fuzzy.length === 0)
|
|
129
|
+
return null;
|
|
130
|
+
fuzzy.sort((a, b) => a.length - b.length);
|
|
131
|
+
return fuzzy[0];
|
|
132
|
+
}
|
|
133
|
+
/** glm5.2 ↔ glm-5.2 ↔ GLM5.2 */
|
|
134
|
+
export function modelFuzzyEqual(a, b) {
|
|
135
|
+
return normalizeModelId(a) === normalizeModelId(b);
|
|
136
|
+
}
|
|
137
|
+
export function normalizeModelId(id) {
|
|
138
|
+
return id.toLowerCase().replace(/[_\s./-]+/g, "");
|
|
139
|
+
}
|
|
140
|
+
export function resolveBinary(tool) {
|
|
141
|
+
const fromEnv = process.env[BINARY_ENV[tool]]?.trim();
|
|
142
|
+
if (fromEnv)
|
|
143
|
+
return fromEnv;
|
|
144
|
+
return BINARY[tool];
|
|
145
|
+
}
|
|
146
|
+
export function which(command) {
|
|
147
|
+
if (command.includes("/") || command.includes("\\")) {
|
|
148
|
+
return existsSync(command) ? command : null;
|
|
149
|
+
}
|
|
150
|
+
const pathEnv = process.env.PATH || "";
|
|
151
|
+
for (const dir of pathEnv.split(delimiter)) {
|
|
152
|
+
if (!dir)
|
|
153
|
+
continue;
|
|
154
|
+
const candidate = join(dir, command);
|
|
155
|
+
if (existsSync(candidate))
|
|
156
|
+
return candidate;
|
|
157
|
+
for (const ext of [".cmd", ".exe", ".bat"]) {
|
|
158
|
+
const win = candidate + ext;
|
|
159
|
+
if (existsSync(win))
|
|
160
|
+
return win;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
function spawnInherited(binary, args) {
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
const child = spawn(binary, args, {
|
|
168
|
+
stdio: "inherit",
|
|
169
|
+
env: process.env,
|
|
170
|
+
shell: process.platform === "win32",
|
|
171
|
+
});
|
|
172
|
+
child.on("error", reject);
|
|
173
|
+
child.on("close", (code) => resolve(code));
|
|
174
|
+
});
|
|
175
|
+
}
|