@nvae/llmswitch 0.2.0 → 0.5.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/README.md +111 -197
- package/dist/adapters/claude.js +8 -5
- package/dist/adapters/codex.js +10 -4
- package/dist/bridge/manager.js +241 -146
- package/dist/bridge/runtime.js +199 -0
- package/dist/bridge/server.js +207 -80
- package/dist/bridge/state.js +345 -69
- package/dist/bridge/translate-response.js +205 -80
- package/dist/bridge/transport.js +439 -0
- package/dist/cli.js +7 -1
- package/dist/commands/bridge-cmd.js +15 -13
- package/dist/commands/home-cmd.js +8 -0
- package/dist/commands/launch-cmd.js +18 -0
- package/dist/commands/launch.js +2 -2
- package/dist/commands/prompts.js +157 -116
- package/dist/commands/setup-cmd.js +175 -0
- package/dist/commands/tool.js +18 -33
- package/dist/index.js +0 -0
- package/dist/presets/index.js +10 -1
- package/dist/store/profiles.js +40 -2
- package/dist/types.js +20 -3
- package/dist/utils/detect-format.js +178 -0
- package/dist/utils/fetch-models.js +54 -59
- package/dist/utils/proxy.js +14 -37
- package/dist/utils/version.js +9 -0
- package/package.json +6 -3
package/dist/store/profiles.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { isApiFormat } from "../types.js";
|
|
3
|
+
import { isApiFormat, normalizeProxyValue } from "../types.js";
|
|
4
4
|
import { normalizeBaseUrlForFormat } from "../utils/base-url.js";
|
|
5
5
|
import { atomicWriteFile, ensureDir, maskSecret } from "../utils/fs.js";
|
|
6
6
|
import { getProfilePath, getProfilesDir, getStatePath, getToolStoreDir, } from "../utils/paths.js";
|
|
@@ -63,6 +63,44 @@ export function requireProfile(tool, name) {
|
|
|
63
63
|
}
|
|
64
64
|
return profile;
|
|
65
65
|
}
|
|
66
|
+
/** 归一化引用串:小写并去掉分隔符(kimi-openai ↔ kimiOpenai)。 */
|
|
67
|
+
export function normalizeReference(value) {
|
|
68
|
+
return value.toLowerCase().replace(/[_\s./-]+/g, "");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 按用户输入解析 profile:优先名称精确匹配,其次显示名称精确匹配,
|
|
72
|
+
* 再尝试归一化(大小写/分隔符)与包含匹配;多义时返回 null。
|
|
73
|
+
*/
|
|
74
|
+
export function resolveProfile(tool, query) {
|
|
75
|
+
const trimmed = query.trim();
|
|
76
|
+
if (!trimmed)
|
|
77
|
+
return null;
|
|
78
|
+
const byName = readProfile(tool, trimmed);
|
|
79
|
+
if (byName)
|
|
80
|
+
return byName;
|
|
81
|
+
const profiles = listProfiles(tool);
|
|
82
|
+
const normalized = normalizeReference(trimmed);
|
|
83
|
+
const exactDisplay = profiles.find((p) => normalizeReference(p.displayName) === normalized);
|
|
84
|
+
if (exactDisplay)
|
|
85
|
+
return exactDisplay;
|
|
86
|
+
const fuzzy = profiles.filter((p) => normalizeReference(p.displayName).includes(normalized) ||
|
|
87
|
+
normalizeReference(p.name).includes(normalized));
|
|
88
|
+
return fuzzy.length === 1 ? fuzzy[0] : null;
|
|
89
|
+
}
|
|
90
|
+
export function resolveProfileOrThrow(tool, query) {
|
|
91
|
+
const profile = resolveProfile(tool, query);
|
|
92
|
+
if (!profile) {
|
|
93
|
+
const profiles = listProfiles(tool);
|
|
94
|
+
if (profiles.length === 0) {
|
|
95
|
+
throw new Error(`暂无 ${tool} 供应商。请先:llms ${tool} provider`);
|
|
96
|
+
}
|
|
97
|
+
const names = profiles
|
|
98
|
+
.map((p) => `${p.name}(${p.displayName})`)
|
|
99
|
+
.join(", ");
|
|
100
|
+
throw new Error(`未找到匹配「${query}」的供应商。现有:${names}。可通过名称或显示名称引用。`);
|
|
101
|
+
}
|
|
102
|
+
return profile;
|
|
103
|
+
}
|
|
66
104
|
export function saveProfile(tool, profile) {
|
|
67
105
|
assertValidProfileName(profile.name);
|
|
68
106
|
if (!isApiFormat(profile.apiFormat)) {
|
|
@@ -191,7 +229,7 @@ function normalizeProfile(raw, fallbackName) {
|
|
|
191
229
|
fast: raw.models?.fast || undefined,
|
|
192
230
|
list: list.length ? list : raw.models?.default ? [raw.models.default] : [],
|
|
193
231
|
},
|
|
194
|
-
proxy: raw.proxy,
|
|
232
|
+
proxy: normalizeProxyValue(raw.proxy),
|
|
195
233
|
bridgeMode: raw.bridgeMode,
|
|
196
234
|
headers: raw.headers || {},
|
|
197
235
|
updatedAt: raw.updatedAt || new Date(0).toISOString(),
|
package/dist/types.js
CHANGED
|
@@ -11,7 +11,24 @@ export function isApiFormat(value) {
|
|
|
11
11
|
return API_FORMATS.includes(value);
|
|
12
12
|
}
|
|
13
13
|
export function emptyProxy(proxy) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
return !proxy || !proxy.trim();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Coerce a stored proxy value into a single URL string. Accepts the current
|
|
18
|
+
* string form, or the legacy `{ http, https, all }` object (preferring `all`,
|
|
19
|
+
* then `https`, then `http`). Returns undefined when no proxy is set.
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeProxyValue(raw) {
|
|
22
|
+
if (typeof raw === "string") {
|
|
23
|
+
const trimmed = raw.trim();
|
|
24
|
+
return trimmed ? trimmed : undefined;
|
|
25
|
+
}
|
|
26
|
+
if (raw && typeof raw === "object") {
|
|
27
|
+
const row = raw;
|
|
28
|
+
for (const value of [row.all, row.https, row.http]) {
|
|
29
|
+
if (typeof value === "string" && value.trim())
|
|
30
|
+
return value.trim();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
17
34
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { supportedFormats } from "../formats/compatibility.js";
|
|
2
|
+
import { requestWithNodeTransport } from "../bridge/transport.js";
|
|
3
|
+
import { baseUrlFromModelsEndpoint, buildModelsRequestHeaders, modelListEndpoints, parseModelIds, } from "./fetch-models.js";
|
|
4
|
+
const PROBE_TIMEOUT_MS = 4000;
|
|
5
|
+
async function probe(url, method, headers, proxy, body) {
|
|
6
|
+
const controller = new AbortController();
|
|
7
|
+
const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
8
|
+
try {
|
|
9
|
+
const res = await requestWithNodeTransport({
|
|
10
|
+
url,
|
|
11
|
+
method,
|
|
12
|
+
headers,
|
|
13
|
+
proxy,
|
|
14
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
15
|
+
signal: controller.signal,
|
|
16
|
+
totalTimeoutMs: PROBE_TIMEOUT_MS,
|
|
17
|
+
});
|
|
18
|
+
let json = null;
|
|
19
|
+
try {
|
|
20
|
+
json = await res.json();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return { status: res.status, json };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** GET /models(Anthropic 头)成功且返回模型 → anthropic 兼容。 */
|
|
35
|
+
async function probeAnthropicModels(baseUrl, apiKey, proxy) {
|
|
36
|
+
const headers = buildModelsRequestHeaders("anthropic", apiKey);
|
|
37
|
+
for (const endpoint of modelListEndpoints(baseUrl)) {
|
|
38
|
+
const res = await probe(endpoint, "GET", headers, proxy);
|
|
39
|
+
if (res &&
|
|
40
|
+
res.status >= 200 &&
|
|
41
|
+
res.status < 300 &&
|
|
42
|
+
parseModelIds(res.json).length > 0) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
/** GET /models(Bearer 头)成功 → OpenAI 兼容。 */
|
|
49
|
+
async function probeOpenAiModels(baseUrl, apiKey, proxy) {
|
|
50
|
+
const headers = buildModelsRequestHeaders("openai-chat", apiKey);
|
|
51
|
+
for (const endpoint of modelListEndpoints(baseUrl)) {
|
|
52
|
+
const res = await probe(endpoint, "GET", headers, proxy);
|
|
53
|
+
if (res &&
|
|
54
|
+
res.status >= 200 &&
|
|
55
|
+
res.status < 300 &&
|
|
56
|
+
parseModelIds(res.json).length > 0) {
|
|
57
|
+
return endpoint;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* POST 探测某一路由是否存在:404/501 或非 JSON 响应视为不存在;
|
|
64
|
+
* 400/401/422/200 等带 JSON body 的响应视为路由存在。
|
|
65
|
+
*/
|
|
66
|
+
async function routeExists(baseUrl, suffix, apiKey, proxy, body) {
|
|
67
|
+
const base = baseUrl.trim().replace(/\/+$/, "");
|
|
68
|
+
const candidates = [];
|
|
69
|
+
if (/\/v1$/i.test(base)) {
|
|
70
|
+
candidates.push(`${base}/${suffix}`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
candidates.push(`${base}/${suffix}`);
|
|
74
|
+
candidates.push(`${base}/v1/${suffix}`);
|
|
75
|
+
}
|
|
76
|
+
const headers = {
|
|
77
|
+
Accept: "application/json",
|
|
78
|
+
"Content-Type": "application/json",
|
|
79
|
+
};
|
|
80
|
+
if (apiKey.trim())
|
|
81
|
+
headers.Authorization = `Bearer ${apiKey.trim()}`;
|
|
82
|
+
for (const endpoint of candidates) {
|
|
83
|
+
const res = await probe(endpoint, "POST", headers, proxy, body);
|
|
84
|
+
if (!res)
|
|
85
|
+
continue;
|
|
86
|
+
if (res.status === 404 || res.status === 501 || res.status === 405) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (res.json === null)
|
|
90
|
+
continue;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
/** GET /api/tags(Ollama 原生)成功且返回模型 → 本地 Ollama。 */
|
|
96
|
+
async function probeOllamaTags(baseUrl, proxy) {
|
|
97
|
+
const base = baseUrl.trim().replace(/\/+$/, "");
|
|
98
|
+
const endpoint = `${base}/api/tags`;
|
|
99
|
+
const res = await probe(endpoint, "GET", { Accept: "application/json" }, proxy);
|
|
100
|
+
return (res !== null &&
|
|
101
|
+
res.status >= 200 &&
|
|
102
|
+
res.status < 300 &&
|
|
103
|
+
parseModelIds(res.json).length > 0);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 自动识别上游接口类型。探测策略(每个请求 4s 超时,串行):
|
|
107
|
+
* 1. Anthropic /models(有 Key 且工具支持时,claude/opencode 优先原生)
|
|
108
|
+
* 2. OpenAI /models(无 Key 也探测,覆盖 Ollama /vLLM 等本地服务)
|
|
109
|
+
* 3. Ollama 原生 /api/tags(兼容 OpenAI 兼容层缺失的旧版本)
|
|
110
|
+
* 4. /models 全失败且有 Key 时兜底探测 /v1/chat/completions 路由
|
|
111
|
+
* 全部失败返回 detected: false,由调用方回退到人工选择。
|
|
112
|
+
*/
|
|
113
|
+
export async function detectApiFormat(tool, opts) {
|
|
114
|
+
const base = opts.baseUrl.trim();
|
|
115
|
+
const key = opts.apiKey.trim();
|
|
116
|
+
const supported = supportedFormats(tool);
|
|
117
|
+
const fallback = {
|
|
118
|
+
apiFormat: "openai-chat",
|
|
119
|
+
detected: false,
|
|
120
|
+
};
|
|
121
|
+
// 没有 URL 无法探测
|
|
122
|
+
if (!base)
|
|
123
|
+
return fallback;
|
|
124
|
+
const chatResult = {
|
|
125
|
+
apiFormat: "openai-chat",
|
|
126
|
+
bridgeMode: tool === "codex" ? "chat" : undefined,
|
|
127
|
+
detected: true,
|
|
128
|
+
source: "openai-models",
|
|
129
|
+
};
|
|
130
|
+
if (key && supported.includes("anthropic")) {
|
|
131
|
+
const antOk = await probeAnthropicModels(base, key, opts.proxy);
|
|
132
|
+
if (antOk) {
|
|
133
|
+
return {
|
|
134
|
+
apiFormat: "anthropic",
|
|
135
|
+
detected: true,
|
|
136
|
+
source: "anthropic",
|
|
137
|
+
resolvedBaseUrl: base.replace(/\/+$/, ""),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const modelsEndpoint = await probeOpenAiModels(base, key, opts.proxy);
|
|
142
|
+
if (modelsEndpoint) {
|
|
143
|
+
const resolvedBaseUrl = baseUrlFromModelsEndpoint(modelsEndpoint) || base.replace(/\/+$/, "");
|
|
144
|
+
// 无 Key 时不做 POST 探测(本地服务默认 chat 最兼容)
|
|
145
|
+
if (key && supported.includes("openai-responses")) {
|
|
146
|
+
const responsesOk = await routeExists(resolvedBaseUrl, "responses", key, opts.proxy, { model: "", input: "hi" });
|
|
147
|
+
if (responsesOk) {
|
|
148
|
+
return {
|
|
149
|
+
apiFormat: "openai-responses",
|
|
150
|
+
detected: true,
|
|
151
|
+
source: "openai-models",
|
|
152
|
+
resolvedBaseUrl,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { ...chatResult, resolvedBaseUrl };
|
|
157
|
+
}
|
|
158
|
+
// Ollama 原生 API(/v1 兼容层可能缺失):映射到 OpenAI Chat + /v1
|
|
159
|
+
if (supported.includes("openai-chat")) {
|
|
160
|
+
const ollamaOk = await probeOllamaTags(base, opts.proxy);
|
|
161
|
+
if (ollamaOk) {
|
|
162
|
+
return {
|
|
163
|
+
...chatResult,
|
|
164
|
+
source: "ollama-tags",
|
|
165
|
+
resolvedBaseUrl: `${base.replace(/\/+$/, "")}/v1`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (key && supported.includes("openai-chat")) {
|
|
170
|
+
const chatOk = await routeExists(base, "chat/completions", key, opts.proxy, {
|
|
171
|
+
model: "",
|
|
172
|
+
messages: [],
|
|
173
|
+
});
|
|
174
|
+
if (chatOk)
|
|
175
|
+
return { ...chatResult, source: "openai-route" };
|
|
176
|
+
}
|
|
177
|
+
return fallback;
|
|
178
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { requestWithNodeTransport } from "../bridge/transport.js";
|
|
2
2
|
import { normalizeBaseUrlForFormat } from "./base-url.js";
|
|
3
|
-
import { buildProxyEnv } from "./proxy.js";
|
|
4
3
|
/**
|
|
5
4
|
* Derive the API base URL from a successful .../models endpoint.
|
|
6
5
|
*/
|
|
@@ -45,20 +44,28 @@ export function modelListEndpoints(baseUrl) {
|
|
|
45
44
|
}
|
|
46
45
|
return endpoints;
|
|
47
46
|
}
|
|
48
|
-
export function buildModelsRequestHeaders(apiFormat, apiKey) {
|
|
47
|
+
export function buildModelsRequestHeaders(apiFormat, apiKey, customHeaders = {}) {
|
|
49
48
|
const headers = {
|
|
50
49
|
Accept: "application/json",
|
|
51
50
|
};
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
if (apiKey) {
|
|
52
|
+
if (apiFormat === "anthropic") {
|
|
53
|
+
headers["x-api-key"] = apiKey;
|
|
54
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
55
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
59
|
+
}
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
for (const [name, value] of Object.entries(customHeaders)) {
|
|
62
|
+
if (/[\r\n]/.test(name) || /[\r\n]/.test(value)) {
|
|
63
|
+
throw new Error(`无效的模型请求 header: ${name}`);
|
|
64
|
+
}
|
|
65
|
+
const existing = Object.keys(headers).find((key) => key.toLowerCase() === name.toLowerCase());
|
|
66
|
+
if (existing)
|
|
67
|
+
delete headers[existing];
|
|
68
|
+
headers[name] = value;
|
|
62
69
|
}
|
|
63
70
|
return headers;
|
|
64
71
|
}
|
|
@@ -93,51 +100,58 @@ export function parseModelIds(payload) {
|
|
|
93
100
|
* Tries several common /models paths; uses proxy env when configured.
|
|
94
101
|
*/
|
|
95
102
|
export async function fetchModelList(options) {
|
|
96
|
-
const { baseUrl, apiKey, apiFormat, proxy, timeoutMs = 20_000 } = options;
|
|
103
|
+
const { baseUrl, apiKey, apiFormat, proxy, headers: customHeaders, timeoutMs = 20_000, } = options;
|
|
97
104
|
if (!baseUrl?.trim()) {
|
|
98
105
|
throw new Error("Base URL 为空,无法拉取模型列表");
|
|
99
106
|
}
|
|
100
|
-
if (!apiKey?.trim()) {
|
|
101
|
-
throw new Error("API Key 为空,无法拉取模型列表");
|
|
102
|
-
}
|
|
103
107
|
const effectiveBaseUrl = normalizeBaseUrlForFormat(apiFormat, baseUrl);
|
|
104
108
|
const endpoints = modelListEndpoints(effectiveBaseUrl);
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
// 无 Key 的本地服务(如 Ollama 原生 API)兜底探测 /api/tags
|
|
110
|
+
if (!apiKey?.trim()) {
|
|
111
|
+
const root = effectiveBaseUrl
|
|
112
|
+
.replace(/\/v1$/i, "")
|
|
113
|
+
.replace(/\/+$/, "");
|
|
114
|
+
const tags = `${root}/api/tags`;
|
|
115
|
+
if (!endpoints.includes(tags))
|
|
116
|
+
endpoints.push(tags);
|
|
117
|
+
}
|
|
118
|
+
const headers = buildModelsRequestHeaders(apiFormat, apiKey.trim(), customHeaders);
|
|
107
119
|
const errors = [];
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
catch (err) {
|
|
123
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
124
|
-
errors.push(`${endpoint} → ${msg}`);
|
|
120
|
+
for (const endpoint of endpoints) {
|
|
121
|
+
try {
|
|
122
|
+
const models = await requestModels(endpoint, headers, timeoutMs, proxy);
|
|
123
|
+
if (models.length > 0) {
|
|
124
|
+
// Anthropic Messages base URLs are not implied by a sibling /v1/models
|
|
125
|
+
// endpoint; keep the operator-provided base for those.
|
|
126
|
+
const resolvedBaseUrl = apiFormat === "anthropic"
|
|
127
|
+
? effectiveBaseUrl
|
|
128
|
+
: baseUrlFromModelsEndpoint(endpoint) || effectiveBaseUrl;
|
|
129
|
+
return {
|
|
130
|
+
models,
|
|
131
|
+
endpoint,
|
|
132
|
+
resolvedBaseUrl: normalizeBaseUrlForFormat(apiFormat, resolvedBaseUrl),
|
|
133
|
+
};
|
|
125
134
|
}
|
|
135
|
+
errors.push(`${endpoint} → 返回空列表`);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
139
|
+
errors.push(`${endpoint} → ${msg}`);
|
|
126
140
|
}
|
|
127
|
-
}
|
|
128
|
-
finally {
|
|
129
|
-
restore();
|
|
130
141
|
}
|
|
131
142
|
throw new Error(`无法从接口拉取模型列表。\n${errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
132
143
|
}
|
|
133
|
-
async function requestModels(endpoint, headers, timeoutMs) {
|
|
144
|
+
async function requestModels(endpoint, headers, timeoutMs, proxy) {
|
|
134
145
|
const controller = new AbortController();
|
|
135
146
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
136
147
|
try {
|
|
137
|
-
const res = await
|
|
148
|
+
const res = await requestWithNodeTransport({
|
|
149
|
+
url: endpoint,
|
|
138
150
|
method: "GET",
|
|
139
151
|
headers,
|
|
152
|
+
proxy,
|
|
140
153
|
signal: controller.signal,
|
|
154
|
+
totalTimeoutMs: timeoutMs,
|
|
141
155
|
});
|
|
142
156
|
if (!res.ok) {
|
|
143
157
|
const body = (await res.text().catch(() => "")).slice(0, 200);
|
|
@@ -156,22 +170,3 @@ async function requestModels(endpoint, headers, timeoutMs) {
|
|
|
156
170
|
clearTimeout(timer);
|
|
157
171
|
}
|
|
158
172
|
}
|
|
159
|
-
function applyProxyEnv(proxy) {
|
|
160
|
-
if (emptyProxy(proxy))
|
|
161
|
-
return () => undefined;
|
|
162
|
-
const next = buildProxyEnv(proxy);
|
|
163
|
-
const keys = Object.keys(next);
|
|
164
|
-
const backup = new Map();
|
|
165
|
-
for (const key of keys) {
|
|
166
|
-
backup.set(key, process.env[key]);
|
|
167
|
-
process.env[key] = next[key];
|
|
168
|
-
}
|
|
169
|
-
return () => {
|
|
170
|
-
for (const [key, value] of backup) {
|
|
171
|
-
if (value === undefined)
|
|
172
|
-
delete process.env[key];
|
|
173
|
-
else
|
|
174
|
-
process.env[key] = value;
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
}
|
package/dist/utils/proxy.js
CHANGED
|
@@ -8,39 +8,23 @@ export const PROXY_ENV_KEYS = [
|
|
|
8
8
|
"all_proxy",
|
|
9
9
|
];
|
|
10
10
|
/**
|
|
11
|
-
* Build proxy env vars for injection into tool configs.
|
|
12
|
-
*
|
|
13
|
-
* set
|
|
14
|
-
*
|
|
11
|
+
* Build proxy env vars for injection into tool configs. A single proxy URL is
|
|
12
|
+
* applied to all traffic, so HTTP_PROXY, HTTPS_PROXY and ALL_PROXY (plus their
|
|
13
|
+
* lowercase forms) are all set to that URL. SOCKS URLs are honored by runtimes
|
|
14
|
+
* that read these variables.
|
|
15
15
|
*/
|
|
16
16
|
export function buildProxyEnv(proxy) {
|
|
17
17
|
if (emptyProxy(proxy))
|
|
18
18
|
return {};
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
env.HTTP_PROXY = http;
|
|
29
|
-
env.http_proxy = http;
|
|
30
|
-
}
|
|
31
|
-
else if (all) {
|
|
32
|
-
env.HTTP_PROXY = all;
|
|
33
|
-
env.http_proxy = all;
|
|
34
|
-
}
|
|
35
|
-
if (https) {
|
|
36
|
-
env.HTTPS_PROXY = https;
|
|
37
|
-
env.https_proxy = https;
|
|
38
|
-
}
|
|
39
|
-
else if (all) {
|
|
40
|
-
env.HTTPS_PROXY = all;
|
|
41
|
-
env.https_proxy = all;
|
|
42
|
-
}
|
|
43
|
-
return env;
|
|
19
|
+
const url = proxy.trim();
|
|
20
|
+
return {
|
|
21
|
+
HTTP_PROXY: url,
|
|
22
|
+
HTTPS_PROXY: url,
|
|
23
|
+
ALL_PROXY: url,
|
|
24
|
+
http_proxy: url,
|
|
25
|
+
https_proxy: url,
|
|
26
|
+
all_proxy: url,
|
|
27
|
+
};
|
|
44
28
|
}
|
|
45
29
|
export function clearProxyEnvKeys(env) {
|
|
46
30
|
for (const key of PROXY_ENV_KEYS) {
|
|
@@ -57,12 +41,5 @@ export function applyProxyToEnvRecord(env, proxy) {
|
|
|
57
41
|
export function formatProxySummary(proxy) {
|
|
58
42
|
if (emptyProxy(proxy))
|
|
59
43
|
return "(none)";
|
|
60
|
-
|
|
61
|
-
if (proxy?.http)
|
|
62
|
-
parts.push(`http=${proxy.http}`);
|
|
63
|
-
if (proxy?.https)
|
|
64
|
-
parts.push(`https=${proxy.https}`);
|
|
65
|
-
if (proxy?.all)
|
|
66
|
-
parts.push(`all=${proxy.all}`);
|
|
67
|
-
return parts.join(", ");
|
|
44
|
+
return proxy.trim();
|
|
68
45
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const packageJsonPath = resolve(__dirname, "../../package.json");
|
|
6
|
+
export function getVersion() {
|
|
7
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
8
|
+
return pkg.version ?? "unknown";
|
|
9
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nvae/llmswitch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "CLI to switch LLM providers and models for Claude Code, Codex, and OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"llms": "dist/index.js",
|
|
8
|
-
"llm-switch": "dist/index.js"
|
|
8
|
+
"llm-switch": "dist/index.js",
|
|
9
|
+
"llmswitch": "dist/index.js"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"dist",
|
|
@@ -39,7 +40,9 @@
|
|
|
39
40
|
"dependencies": {
|
|
40
41
|
"@clack/prompts": "^0.11.0",
|
|
41
42
|
"commander": "^14.0.0",
|
|
42
|
-
"
|
|
43
|
+
"https-proxy-agent": "7.0.6",
|
|
44
|
+
"smol-toml": "^1.4.2",
|
|
45
|
+
"socks-proxy-agent": "8.0.5"
|
|
43
46
|
},
|
|
44
47
|
"devDependencies": {
|
|
45
48
|
"@types/bun": "^1.2.19",
|