llm-api-gateway-cli 1.0.4 → 1.0.6

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.
@@ -24,7 +24,9 @@ import { fileURLToPath } from 'node:url';
24
24
  import path from 'node:path';
25
25
 
26
26
  import { loadDotEnv, DEFAULT_BASE_URL, sessionBaggage } from './lib/common.js';
27
- import { resolveConfig, KEY_HINT } from './lib/config.js';
27
+ import { resolveConfig, keyHint } from './lib/config.js';
28
+ // CLI 侧语言(只覆盖命令行输出;Web 界面与手册页仍是中文)。默认 zh,见 lib/i18n.js
29
+ import { pick, setLang, resolveLang, LANG_FLAG } from './lib/i18n.js';
28
30
 
29
31
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
30
32
  loadDotEnv(SCRIPT_DIR); // 先加载 .env,让 GATEWAY_MODEL 等环境变量参与默认值计算
@@ -32,7 +34,7 @@ loadDotEnv(SCRIPT_DIR); // 先加载 .env,让 GATEWAY_MODEL 等环境变量参
32
34
  const DEFAULT_MODEL = process.env.GATEWAY_MODEL || 'qwen3:8b';
33
35
  const DEFAULT_HAIKU = 'qwen3:8b-nothink';
34
36
 
35
- const HELP = `用法:node cli-claude-code.js [选项] [-- claude 参数...]
37
+ const HELP_ZH = `用法:node cli-claude-code.js [选项] [-- claude 参数...]
36
38
 
37
39
  选项(本脚本自身):
38
40
  -k, --key <sk-密钥> 网关发放的密钥(可用环境变量 SK/GATEWAY_KEY/ANTHROPIC_AUTH_TOKEN 或 .env)
@@ -45,12 +47,38 @@ const HELP = `用法:node cli-claude-code.js [选项] [-- claude 参数...]
45
47
  --smoke 仅做一次 /v1/messages 连通性自检后退出,不拉起 claude
46
48
  --print-env 仅打印要注入的环境变量,不拉起 claude
47
49
  --config 打印 CC Switch / CC GUI 可直接粘贴的 JSON 配置
50
+ --lang <zh|en> 输出语言(默认 zh,也可用环境变量 LLM_GATEWAY_LANG)
48
51
  -h, --help 显示帮助
49
52
 
50
53
  透传:\`--\` 之后的参数原样交给 claude,例如:
51
54
  node cli-claude-code.js -- -p "你好" --model deepseek-v4-pro
52
55
  `;
53
56
 
57
+ const HELP_EN = `Usage: node cli-claude-code.js [options] [-- claude args...]
58
+
59
+ Options (this script's own):
60
+ -k, --key <sk-key> gateway API key (or env SK/GATEWAY_KEY/ANTHROPIC_AUTH_TOKEN, or .env)
61
+ --base-url <url> gateway base URL (default ${DEFAULT_BASE_URL})
62
+ -m, --model <model> ANTHROPIC_MODEL, the main model (default ${DEFAULT_MODEL})
63
+ --opus-model <model> ANTHROPIC_DEFAULT_OPUS_MODEL (default: same as --model)
64
+ --sonnet-model <model> ANTHROPIC_DEFAULT_SONNET_MODEL (default: same as --model)
65
+ --haiku-model <model> ANTHROPIC_DEFAULT_HAIKU_MODEL (default ${DEFAULT_HAIKU})
66
+ --small-fast-model <model> ANTHROPIC_SMALL_FAST_MODEL, background fast model (default: same as haiku)
67
+ --smoke run one /v1/messages connectivity check, then exit; do not launch claude
68
+ --print-env only print the environment variables to inject; do not launch claude
69
+ --config print JSON config ready to paste into CC Switch / CC GUI
70
+ --lang <zh|en> output language (default zh; env LLM_GATEWAY_LANG)
71
+ -h, --help show this help
72
+
73
+ Passthrough: everything after \`--\` is handed to claude unchanged, e.g.:
74
+ node cli-claude-code.js -- -p "hello" --model deepseek-v4-pro
75
+ `;
76
+
77
+ /** 大段帮助取词必须在**调用时**(模块顶层调 pick 会早于 setLang,永远拿到中文) */
78
+ function help() {
79
+ return pick(HELP_ZH, HELP_EN);
80
+ }
81
+
54
82
  // ---------- 参数解析:本脚本自己的参数 vs 透传给 claude 的参数 ----------
55
83
  // 注意这里不能直接用 lib/common.js 的 parseArgs:本脚本要把「不认识的参数」
56
84
  // 原样透传给 claude,而通用解析器会把它们吃掉。
@@ -65,6 +93,7 @@ const WITH_VALUE = {
65
93
  '--small-fast-model': 'smallFastModel',
66
94
  '-k': 'key',
67
95
  '-m': 'model',
96
+ ...LANG_FLAG, // --lang <zh|en>:本脚本自己消费,不透传给 claude
68
97
  };
69
98
  const FLAG = {
70
99
  '--smoke': 'smoke',
@@ -154,6 +183,7 @@ async function smoke(baseUrl, key, model) {
154
183
  const url = `${baseUrl.replace(/\/+$/, '')}/v1/messages`;
155
184
  // 用流式请求自检:Claude Code 实际走的就是 /v1/messages 的 SSE 流,最能反映真实可用性
156
185
  // max_tokens 给足:本地 thinking 模型(如 qwen3:8b)会先把额度花在 reasoning 上,太小则看不到正文
186
+ // 正文这句是**发给网关的请求内容**(不是给人看的输出),一律保持中文,不随 --lang 变
157
187
  const body = { model, max_tokens: 256, messages: [{ role: 'user', content: '你好,请用一句话回复' }], stream: true };
158
188
  let res;
159
189
  try {
@@ -167,12 +197,12 @@ async function smoke(baseUrl, key, model) {
167
197
  body: JSON.stringify(body),
168
198
  });
169
199
  } catch (e) {
170
- console.error(`[smoke] 连接异常: ${e?.message || e}`);
200
+ console.error(pick(`[smoke] 连接异常: ${e?.message || e}`, `[smoke] connection error: ${e?.message || e}`));
171
201
  return false;
172
202
  }
173
203
  if (!res.ok) {
174
204
  const text = await res.text();
175
- console.error(`[smoke] 失败 HTTP ${res.status}: ${text.slice(0, 300)}`);
205
+ console.error(pick(`[smoke] 失败 HTTP ${res.status}: ${text.slice(0, 300)}`, `[smoke] failed HTTP ${res.status}: ${text.slice(0, 300)}`));
176
206
  return false;
177
207
  }
178
208
 
@@ -210,10 +240,13 @@ async function smoke(baseUrl, key, model) {
210
240
  }
211
241
  }
212
242
  if (!started) {
213
- console.error('[smoke] 失败:未收到 message_start 事件');
243
+ console.error(pick('[smoke] 失败:未收到 message_start 事件', '[smoke] failed: no message_start event received'));
214
244
  return false;
215
245
  }
216
- console.error(`[smoke] 成功 model=${model} 回复="${reply.slice(0, 40)}"`);
246
+ console.error(pick(
247
+ `[smoke] 成功 model=${model} 回复="${reply.slice(0, 40)}"`,
248
+ `[smoke] ok model=${model} reply="${reply.slice(0, 40)}"`,
249
+ ));
217
250
  return true;
218
251
  }
219
252
 
@@ -225,13 +258,16 @@ function runClaude(passthrough, env) {
225
258
  if (!r.error) return r.status ?? 0;
226
259
  // Windows 下 npm 安装的 claude 是 .cmd shim,直接 spawn 会报 ENOENT/EINVAL → 回退 shell 解析
227
260
  if (r.error.code !== 'ENOENT' && r.error.code !== 'EINVAL') {
228
- console.error(`[错误] 启动 claude 失败: ${r.error.message}`);
261
+ console.error(pick(`[错误] 启动 claude 失败: ${r.error.message}`, `[error] failed to launch claude: ${r.error.message}`));
229
262
  return 1;
230
263
  }
231
264
  const cmd = ['claude', ...passthrough].map((s) => (/[\s"&|<>]/.test(s) ? JSON.stringify(s) : s)).join(' ');
232
265
  const r2 = spawnSync(cmd, { env, stdio: 'inherit', shell: true });
233
266
  if (r2.error) {
234
- console.error('[错误] 未找到 claude 命令:请先安装 Claude Code(npm install -g @anthropic-ai/claude-code)');
267
+ console.error(pick(
268
+ '[错误] 未找到 claude 命令:请先安装 Claude Code(npm install -g @anthropic-ai/claude-code)',
269
+ '[error] claude command not found: install Claude Code first (npm install -g @anthropic-ai/claude-code)',
270
+ ));
235
271
  return 1;
236
272
  }
237
273
  return r2.status ?? 0;
@@ -241,9 +277,12 @@ function runClaude(passthrough, env) {
241
277
 
242
278
  async function main() {
243
279
  const { opts, passthrough } = parseArgs(process.argv.slice(2));
280
+ // 语言要在**任何输出之前**定下来:--help / 报错 / 横幅都按它取词。
281
+ // 注意 --lang 已在 WITH_VALUE 里,不会被当成 claude 的参数透传出去
282
+ setLang(resolveLang(opts, process.env));
244
283
 
245
284
  if (opts.help) {
246
- console.log(HELP);
285
+ console.log(help());
247
286
  return;
248
287
  }
249
288
 
@@ -254,7 +293,7 @@ async function main() {
254
293
  );
255
294
 
256
295
  if (!key) {
257
- console.error(`[错误] ${KEY_HINT}`);
296
+ console.error(`${pick('[错误]', '[error]')} ${keyHint()}`);
258
297
  process.exit(1);
259
298
  }
260
299
 
@@ -306,10 +345,18 @@ async function main() {
306
345
  return;
307
346
  }
308
347
 
309
- process.stderr.write(`[gateway] 启动 Claude Code:base=${baseUrl} model=${env.ANTHROPIC_MODEL}\n`);
348
+ process.stderr.write(pick(
349
+ `[gateway] 启动 Claude Code:base=${baseUrl} model=${env.ANTHROPIC_MODEL}\n`,
350
+ `[gateway] launching Claude Code: base=${baseUrl} model=${env.ANTHROPIC_MODEL}\n`,
351
+ ));
310
352
  // 把会话 id 显式打出来(与另外三个入口的提示一致),否则任务树里对不上是哪一次启动
311
353
  const sid = /session\.id=([A-Za-z0-9._-]+)/.exec(String(env.ANTHROPIC_CUSTOM_HEADERS || ''));
312
- if (sid) process.stderr.write(`[gateway] 会话 ${sid[1]}(W3C baggage: session.id)\n`);
354
+ if (sid) {
355
+ process.stderr.write(pick(
356
+ `[gateway] 会话 ${sid[1]}(W3C baggage: session.id)\n`,
357
+ `[gateway] session ${sid[1]} (W3C baggage: session.id)\n`,
358
+ ));
359
+ }
313
360
  const code = runClaude(passthrough, env);
314
361
  process.exit(code);
315
362
  }
package/cli-openai.js CHANGED
@@ -28,14 +28,16 @@ import readline from 'node:readline';
28
28
  import { randomUUID } from 'node:crypto';
29
29
 
30
30
  import { loadDotEnv, parseArgs, sessionBaggage, DEFAULT_BASE_URL } from './lib/common.js';
31
- import { resolveConfig, fail, KEY_HINT } from './lib/config.js';
31
+ import { resolveConfig, fail, keyHint } from './lib/config.js';
32
+ // CLI 侧语言(只覆盖命令行输出;Web 界面与手册页仍是中文)。默认 zh,见 lib/i18n.js
33
+ import { pick, setLang, resolveLang, LANG_FLAG } from './lib/i18n.js';
32
34
 
33
35
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
34
36
  loadDotEnv(SCRIPT_DIR); // 先加载 .env,让 GATEWAY_MODEL 等环境变量参与默认值计算
35
37
 
36
38
  const DEFAULT_MODEL = process.env.GATEWAY_MODEL || 'qwen3:8b';
37
39
 
38
- const HELP = `用法:node cli-openai.js [选项] [提示词]
40
+ const HELP_ZH = `用法:node cli-openai.js [选项] [提示词]
39
41
 
40
42
  选项:
41
43
  -p, --prompt <文本> 单轮提问内容(省略则读位置参数 / 标准输入)
@@ -49,9 +51,33 @@ const HELP = `用法:node cli-openai.js [选项] [提示词]
49
51
  --json 输出完整原始响应(非流式)
50
52
  --list-models 列出上游可用模型
51
53
  -i, --interactive 进入多轮交互(REPL)
54
+ --lang <zh|en> 输出语言(默认 zh,也可用环境变量 LLM_GATEWAY_LANG)
52
55
  -h, --help 显示帮助
53
56
  `;
54
57
 
58
+ const HELP_EN = `Usage: node cli-openai.js [options] [prompt]
59
+
60
+ Options:
61
+ -p, --prompt <text> one-shot prompt (falls back to the positional argument / stdin)
62
+ -s, --system <text> system prompt
63
+ -m, --model <model> model name (default ${DEFAULT_MODEL})
64
+ -k, --key <sk-key> gateway API key (or env SK/GATEWAY_KEY/OPENAI_API_KEY, or .env)
65
+ --base-url <url> gateway base URL (default ${DEFAULT_BASE_URL})
66
+ --temperature <v> sampling temperature
67
+ --max-tokens <v> max generated tokens
68
+ --no-stream disable streaming output (on by default)
69
+ --json print the full raw response (non-streaming)
70
+ --list-models list available upstream models
71
+ -i, --interactive multi-turn interactive mode (REPL)
72
+ --lang <zh|en> output language (default zh; env LLM_GATEWAY_LANG)
73
+ -h, --help show this help
74
+ `;
75
+
76
+ /** 大段帮助取词必须在**调用时**(模块顶层调 pick 会早于 setLang,永远拿到中文) */
77
+ function help() {
78
+ return pick(HELP_ZH, HELP_EN);
79
+ }
80
+
55
81
  // 值型选项:会消费下一个参数作为值;布尔型选项:不消费后续参数
56
82
  const VALUE_FLAGS = {
57
83
  '--prompt': 'prompt', '-p': 'prompt',
@@ -61,6 +87,7 @@ const VALUE_FLAGS = {
61
87
  '--base-url': 'base-url',
62
88
  '--temperature': 'temperature',
63
89
  '--max-tokens': 'max-tokens',
90
+ ...LANG_FLAG, // --lang <zh|en>:与另外三个入口同一份定义(lib/i18n.js)
64
91
  };
65
92
  const BOOL_FLAGS = {
66
93
  '--no-stream': 'no-stream',
@@ -86,23 +113,25 @@ function fmtUsage(u) {
86
113
  const p = u.prompt_tokens ?? '?';
87
114
  const c = u.completion_tokens ?? '?';
88
115
  const t = u.total_tokens ?? (Number.isFinite(p) && Number.isFinite(c) ? p + c : '?');
89
- return `输入 ${p} / 输出 ${c} / 合计 ${t} tokens`;
116
+ return pick(`输入 ${p} / 输出 ${c} / 合计 ${t} tokens`, `input ${p} / output ${c} / total ${t} tokens`);
90
117
  }
91
118
 
92
119
  // ---------- 主流程 ----------
93
120
 
94
121
  async function main() {
95
122
  const args = parseArgs(process.argv.slice(2), VALUE_FLAGS, BOOL_FLAGS);
123
+ // 语言要在**任何输出之前**定下来:--help / 报错 / 横幅都按它取词
124
+ setLang(resolveLang(args, process.env));
96
125
 
97
126
  if (args.help) {
98
- console.log(HELP);
127
+ console.log(help());
99
128
  return;
100
129
  }
101
130
 
102
131
  const { key, baseUrl, model, temperature, maxTokens } = resolveConfig(args, { defaultModel: DEFAULT_MODEL });
103
132
 
104
133
  if (!key) {
105
- console.error(`[错误] ${KEY_HINT}`);
134
+ console.error(`${pick('[错误]', '[error]')} ${keyHint()}`);
106
135
  process.exit(1);
107
136
  }
108
137
 
@@ -111,7 +140,12 @@ async function main() {
111
140
  const sessionId = randomUUID();
112
141
  const baggage = sessionBaggage(sessionId);
113
142
  // 打到 stderr,便于在后台「任务树」里按这个 id 对号入座
114
- if (baggage) process.stderr.write(`[gateway] 会话 ${sessionId}(W3C baggage: session.id)\n`);
143
+ if (baggage) {
144
+ process.stderr.write(pick(
145
+ `[gateway] 会话 ${sessionId}(W3C baggage: session.id)\n`,
146
+ `[gateway] session ${sessionId} (W3C baggage: session.id)\n`,
147
+ ));
148
+ }
115
149
 
116
150
  const client = new OpenAI({
117
151
  baseURL: `${baseUrl}/v1`,
@@ -123,7 +157,7 @@ async function main() {
123
157
  try {
124
158
  const res = await client.models.list();
125
159
  const ids = (res.data || []).map((m) => m.id);
126
- console.log(ids.length ? ids.join('\n') : '(上游未返回模型列表)');
160
+ console.log(ids.length ? ids.join('\n') : pick('(上游未返回模型列表)', '(upstream returned no model list)'));
127
161
  } catch (e) {
128
162
  fail(e);
129
163
  }
@@ -141,7 +175,7 @@ async function main() {
141
175
  if (!prompt && args._.length) prompt = args._.join(' ');
142
176
  if (!prompt) prompt = await readStdin();
143
177
  if (!prompt) {
144
- console.log(HELP);
178
+ console.log(help());
145
179
  return;
146
180
  }
147
181
 
@@ -189,10 +223,13 @@ async function chat(client, model, messages, { stream, temperature, maxTokens, j
189
223
  }
190
224
 
191
225
  async function repl(client, model, opts) {
192
- console.error(`[gateway] 交互模式 model=${model},输入文本提问,\`exit\` 退出`);
226
+ console.error(pick(
227
+ `[gateway] 交互模式 model=${model},输入文本提问,\`exit\` 退出`,
228
+ `[gateway] interactive mode model=${model}; type a prompt, \`exit\` to quit`,
229
+ ));
193
230
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
194
231
  const messages = [];
195
- const ask = () => new Promise((resolve) => rl.question('你> ', resolve));
232
+ const ask = () => new Promise((resolve) => rl.question(pick('你> ', 'you> '), resolve));
196
233
  for (;;) {
197
234
  let line;
198
235
  try {
@@ -203,7 +240,7 @@ async function repl(client, model, opts) {
203
240
  if (!line) continue;
204
241
  if (/^(exit|quit)$/i.test(line)) break;
205
242
  messages.push({ role: 'user', content: line });
206
- process.stdout.write('助手> ');
243
+ process.stdout.write(pick('助手> ', 'assistant> '));
207
244
  await chat(client, model, messages, { ...opts, json: false, exitOnError: false });
208
245
  }
209
246
  rl.close();