@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.
@@ -1,6 +1,5 @@
1
- import { emptyProxy } from "../types.js";
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 (!apiKey)
53
- return headers;
54
- if (apiFormat === "anthropic") {
55
- headers["x-api-key"] = apiKey;
56
- headers["anthropic-version"] = "2023-06-01";
57
- // Some gateways also accept Bearer
58
- headers.Authorization = `Bearer ${apiKey}`;
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
- else {
61
- headers.Authorization = `Bearer ${apiKey}`;
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
- try {
109
- for (const endpoint of endpoints) {
110
- try {
111
- const models = await requestModels(endpoint, headers, timeoutMs);
112
- if (models.length > 0) {
113
- const resolvedBaseUrl = baseUrlFromModelsEndpoint(endpoint) || effectiveBaseUrl;
114
- return {
115
- models,
116
- endpoint,
117
- resolvedBaseUrl: normalizeBaseUrlForFormat(apiFormat, resolvedBaseUrl),
118
- };
119
- }
120
- errors.push(`${endpoint} → 返回空列表`);
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 fetch(endpoint, {
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
- }
@@ -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
- * Prefer explicit http/https; when only `all` is set (e.g. socks5h),
13
- * set ALL_PROXY (and lowercase) and also mirror to HTTP(S)_PROXY
14
- * so runtimes that only read those still attempt the proxy URL.
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 env = {};
20
- const http = proxy.http?.trim();
21
- const https = proxy.https?.trim();
22
- const all = proxy.all?.trim();
23
- if (all) {
24
- env.ALL_PROXY = all;
25
- env.all_proxy = all;
26
- }
27
- if (http) {
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
- const parts = [];
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.2.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
- "llm-switch": "dist/index.js"
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
- "smol-toml": "^1.4.2"
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",