@nvae/llmswitch 0.2.0 → 0.4.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 +1 -1
- 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/commands/bridge-cmd.js +13 -11
- package/dist/commands/prompts.js +47 -51
- package/dist/store/profiles.js +2 -2
- package/dist/types.js +20 -3
- package/dist/utils/fetch-models.js +45 -56
- package/dist/utils/proxy.js +14 -37
- package/package.json +5 -3
|
@@ -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,7 +100,7 @@ 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
|
}
|
|
@@ -102,42 +109,43 @@ export async function fetchModelList(options) {
|
|
|
102
109
|
}
|
|
103
110
|
const effectiveBaseUrl = normalizeBaseUrlForFormat(apiFormat, baseUrl);
|
|
104
111
|
const endpoints = modelListEndpoints(effectiveBaseUrl);
|
|
105
|
-
const headers = buildModelsRequestHeaders(apiFormat, apiKey.trim());
|
|
106
|
-
const restore = applyProxyEnv(proxy);
|
|
112
|
+
const headers = buildModelsRequestHeaders(apiFormat, apiKey.trim(), customHeaders);
|
|
107
113
|
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}`);
|
|
114
|
+
for (const endpoint of endpoints) {
|
|
115
|
+
try {
|
|
116
|
+
const models = await requestModels(endpoint, headers, timeoutMs, proxy);
|
|
117
|
+
if (models.length > 0) {
|
|
118
|
+
// Anthropic Messages base URLs are not implied by a sibling /v1/models
|
|
119
|
+
// endpoint; keep the operator-provided base for those.
|
|
120
|
+
const resolvedBaseUrl = apiFormat === "anthropic"
|
|
121
|
+
? effectiveBaseUrl
|
|
122
|
+
: baseUrlFromModelsEndpoint(endpoint) || effectiveBaseUrl;
|
|
123
|
+
return {
|
|
124
|
+
models,
|
|
125
|
+
endpoint,
|
|
126
|
+
resolvedBaseUrl: normalizeBaseUrlForFormat(apiFormat, resolvedBaseUrl),
|
|
127
|
+
};
|
|
125
128
|
}
|
|
129
|
+
errors.push(`${endpoint} → 返回空列表`);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
133
|
+
errors.push(`${endpoint} → ${msg}`);
|
|
126
134
|
}
|
|
127
|
-
}
|
|
128
|
-
finally {
|
|
129
|
-
restore();
|
|
130
135
|
}
|
|
131
136
|
throw new Error(`无法从接口拉取模型列表。\n${errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
132
137
|
}
|
|
133
|
-
async function requestModels(endpoint, headers, timeoutMs) {
|
|
138
|
+
async function requestModels(endpoint, headers, timeoutMs, proxy) {
|
|
134
139
|
const controller = new AbortController();
|
|
135
140
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
136
141
|
try {
|
|
137
|
-
const res = await
|
|
142
|
+
const res = await requestWithNodeTransport({
|
|
143
|
+
url: endpoint,
|
|
138
144
|
method: "GET",
|
|
139
145
|
headers,
|
|
146
|
+
proxy,
|
|
140
147
|
signal: controller.signal,
|
|
148
|
+
totalTimeoutMs: timeoutMs,
|
|
141
149
|
});
|
|
142
150
|
if (!res.ok) {
|
|
143
151
|
const body = (await res.text().catch(() => "")).slice(0, 200);
|
|
@@ -156,22 +164,3 @@ async function requestModels(endpoint, headers, timeoutMs) {
|
|
|
156
164
|
clearTimeout(timer);
|
|
157
165
|
}
|
|
158
166
|
}
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nvae/llmswitch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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
|
-
"
|
|
8
|
+
"llmswitch": "dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@clack/prompts": "^0.11.0",
|
|
41
41
|
"commander": "^14.0.0",
|
|
42
|
-
"
|
|
42
|
+
"https-proxy-agent": "7.0.6",
|
|
43
|
+
"smol-toml": "^1.4.2",
|
|
44
|
+
"socks-proxy-agent": "8.0.5"
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
45
47
|
"@types/bun": "^1.2.19",
|