llm-api-gateway-cli 1.0.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.
Files changed (50) hide show
  1. package/.env.example +10 -0
  2. package/README.md +1127 -0
  3. package/cli-agent.js +666 -0
  4. package/cli-anthropic.js +236 -0
  5. package/cli-claude-code.js +317 -0
  6. package/cli-openai.js +212 -0
  7. package/completions/_llm-api-gateway-cli +65 -0
  8. package/completions/llm-api-gateway-cli.bash +64 -0
  9. package/completions/llm-api-gateway-cli.fish +43 -0
  10. package/images/chat.png +0 -0
  11. package/images/settings.png +0 -0
  12. package/images/task.png +0 -0
  13. package/lib/agent.js +607 -0
  14. package/lib/commands.js +468 -0
  15. package/lib/common.js +196 -0
  16. package/lib/config.js +70 -0
  17. package/lib/configcmd.js +230 -0
  18. package/lib/hub.js +1494 -0
  19. package/lib/jsonstore.js +49 -0
  20. package/lib/mcp.js +375 -0
  21. package/lib/memory.js +109 -0
  22. package/lib/plandoc.js +178 -0
  23. package/lib/pricing.js +52 -0
  24. package/lib/runner.js +234 -0
  25. package/lib/runstore.js +96 -0
  26. package/lib/secrets.js +198 -0
  27. package/lib/sessionstore.js +269 -0
  28. package/lib/settings.js +517 -0
  29. package/lib/tasksession.js +594 -0
  30. package/lib/taskstore.js +740 -0
  31. package/lib/tools.js +927 -0
  32. package/package.json +55 -0
  33. package/public/app.js +1055 -0
  34. package/public/index.html +167 -0
  35. package/public/manual.css +215 -0
  36. package/public/manual.html +381 -0
  37. package/public/manual.js +186 -0
  38. package/public/models.js +121 -0
  39. package/public/render.js +250 -0
  40. package/public/styles.css +955 -0
  41. package/public/task-slash.js +493 -0
  42. package/public/task.css +739 -0
  43. package/public/task.html +220 -0
  44. package/public/task.js +3127 -0
  45. package/public/theme.js +91 -0
  46. package/public/tint.js +261 -0
  47. package/scripts/install.ps1 +537 -0
  48. package/scripts/install.sh +510 -0
  49. package/server.js +14 -0
  50. package/task-server.js +15 -0
package/cli-openai.js ADDED
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 模式一 · OpenAI 兼容接入
4
+ *
5
+ * 通过官方 `openai` SDK 直连网关的 /v1/chat/completions,验证网关可以作为
6
+ * api.openai.com 的 drop-in 替代 —— 客户端只需把 baseURL 指到网关、apiKey
7
+ * 换成网关发放的 sk- 密钥,其余代码完全不变。
8
+ *
9
+ * 用法示例:
10
+ * node cli-openai.js "你好,用一句话介绍你自己"
11
+ * node cli-openai.js -p "你好" --model qwen3:8b --system "你是中文助手"
12
+ * echo "你好" | node cli-openai.js
13
+ * node cli-openai.js --list-models
14
+ * node cli-openai.js -i # 多轮交互
15
+ *
16
+ * 另外:本脚本会额外发一个 W3C `baggage: session.id=<uuid>`(一次运行 = 一次对话),
17
+ * 好让网关后台的「任务树」把同一会话的多次请求聚成一条。这个头是**可选**的 ——
18
+ * 不发(或任何客户端不发)功能完全不受影响,网关会退回自己的推断口径。
19
+ *
20
+ * 密钥来源(优先级从高到低):--key 参数 > 环境变量 SK / GATEWAY_KEY / OPENAI_API_KEY > .env 文件
21
+ * 配置解析统一在 lib/config.js —— 四个入口共用一套优先级,不再各写一份。
22
+ */
23
+
24
+ import OpenAI from 'openai';
25
+ import { fileURLToPath } from 'node:url';
26
+ import path from 'node:path';
27
+ import readline from 'node:readline';
28
+ import { randomUUID } from 'node:crypto';
29
+
30
+ import { loadDotEnv, parseArgs, sessionBaggage, DEFAULT_BASE_URL } from './lib/common.js';
31
+ import { resolveConfig, fail, KEY_HINT } from './lib/config.js';
32
+
33
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
34
+ loadDotEnv(SCRIPT_DIR); // 先加载 .env,让 GATEWAY_MODEL 等环境变量参与默认值计算
35
+
36
+ const DEFAULT_MODEL = process.env.GATEWAY_MODEL || 'qwen3:8b';
37
+
38
+ const HELP = `用法:node cli-openai.js [选项] [提示词]
39
+
40
+ 选项:
41
+ -p, --prompt <文本> 单轮提问内容(省略则读位置参数 / 标准输入)
42
+ -s, --system <文本> system 提示词
43
+ -m, --model <模型> 模型名(默认 ${DEFAULT_MODEL})
44
+ -k, --key <sk-密钥> 网关发放的密钥(也可用环境变量 SK/GATEWAY_KEY/OPENAI_API_KEY 或 .env)
45
+ --base-url <地址> 网关地址(默认 ${DEFAULT_BASE_URL})
46
+ --temperature <值> 采样温度
47
+ --max-tokens <值> 最大生成 token 数
48
+ --no-stream 关闭流式输出(默认开启)
49
+ --json 输出完整原始响应(非流式)
50
+ --list-models 列出上游可用模型
51
+ -i, --interactive 进入多轮交互(REPL)
52
+ -h, --help 显示帮助
53
+ `;
54
+
55
+ // 值型选项:会消费下一个参数作为值;布尔型选项:不消费后续参数
56
+ const VALUE_FLAGS = {
57
+ '--prompt': 'prompt', '-p': 'prompt',
58
+ '--system': 'system', '-s': 'system',
59
+ '--model': 'model', '-m': 'model',
60
+ '--key': 'key', '-k': 'key',
61
+ '--base-url': 'base-url',
62
+ '--temperature': 'temperature',
63
+ '--max-tokens': 'max-tokens',
64
+ };
65
+ const BOOL_FLAGS = {
66
+ '--no-stream': 'no-stream',
67
+ '--json': 'json',
68
+ '--list-models': 'list-models',
69
+ '--interactive': 'interactive', '-i': 'interactive',
70
+ '--help': 'help', '-h': 'help',
71
+ };
72
+
73
+ function readStdin() {
74
+ return new Promise((resolve) => {
75
+ if (process.stdin.isTTY) return resolve('');
76
+ let data = '';
77
+ process.stdin.setEncoding('utf8');
78
+ process.stdin.on('data', (c) => (data += c));
79
+ process.stdin.on('end', () => resolve(data.trim()));
80
+ process.stdin.on('error', () => resolve(''));
81
+ });
82
+ }
83
+
84
+ function fmtUsage(u) {
85
+ if (!u) return null;
86
+ const p = u.prompt_tokens ?? '?';
87
+ const c = u.completion_tokens ?? '?';
88
+ const t = u.total_tokens ?? (Number.isFinite(p) && Number.isFinite(c) ? p + c : '?');
89
+ return `输入 ${p} / 输出 ${c} / 合计 ${t} tokens`;
90
+ }
91
+
92
+ // ---------- 主流程 ----------
93
+
94
+ async function main() {
95
+ const args = parseArgs(process.argv.slice(2), VALUE_FLAGS, BOOL_FLAGS);
96
+
97
+ if (args.help) {
98
+ console.log(HELP);
99
+ return;
100
+ }
101
+
102
+ const { key, baseUrl, model, temperature, maxTokens } = resolveConfig(args, { defaultModel: DEFAULT_MODEL });
103
+
104
+ if (!key) {
105
+ console.error(`[错误] ${KEY_HINT}`);
106
+ process.exit(1);
107
+ }
108
+
109
+ // 一次进程运行 = 一次对话。把这个身份用 W3C baggage 告诉网关,它才能把同一会话的
110
+ // 多次请求聚成一条;这是标准头,不是为网关自造的私有约定,上游不认识也只会原样忽略。
111
+ const sessionId = randomUUID();
112
+ const baggage = sessionBaggage(sessionId);
113
+ // 打到 stderr,便于在后台「任务树」里按这个 id 对号入座
114
+ if (baggage) process.stderr.write(`[gateway] 会话 ${sessionId}(W3C baggage: session.id)\n`);
115
+
116
+ const client = new OpenAI({
117
+ baseURL: `${baseUrl}/v1`,
118
+ apiKey: key,
119
+ ...(baggage ? { defaultHeaders: { baggage } } : {}),
120
+ });
121
+
122
+ if (args['list-models']) {
123
+ try {
124
+ const res = await client.models.list();
125
+ const ids = (res.data || []).map((m) => m.id);
126
+ console.log(ids.length ? ids.join('\n') : '(上游未返回模型列表)');
127
+ } catch (e) {
128
+ fail(e);
129
+ }
130
+ return;
131
+ }
132
+
133
+ const stream = !args['no-stream'];
134
+
135
+ if (args.interactive) {
136
+ await repl(client, model, { stream, temperature, maxTokens, baseUrl });
137
+ return;
138
+ }
139
+
140
+ let prompt = args.prompt ?? '';
141
+ if (!prompt && args._.length) prompt = args._.join(' ');
142
+ if (!prompt) prompt = await readStdin();
143
+ if (!prompt) {
144
+ console.log(HELP);
145
+ return;
146
+ }
147
+
148
+ const messages = [];
149
+ if (args.system) messages.push({ role: 'system', content: args.system });
150
+ messages.push({ role: 'user', content: prompt });
151
+
152
+ await chat(client, model, messages, { stream, temperature, maxTokens, json: args.json, baseUrl });
153
+ }
154
+
155
+ /**
156
+ * @param {boolean} opts.exitOnError 单轮/管道模式为 true(失败要非零退出,脚本才判断得出);
157
+ * 交互模式为 false —— 一次请求失败不该把整个 REPL 结束掉。
158
+ */
159
+ async function chat(client, model, messages, { stream, temperature, maxTokens, json, baseUrl, exitOnError = true }) {
160
+ const req = { model, messages, stream, temperature, max_tokens: maxTokens };
161
+ if (stream) req.stream_options = { include_usage: true }; // 让上游在流末回传 usage
162
+
163
+ process.stderr.write(`[gateway] model=${model} base=${baseUrl} stream=${stream}\n`);
164
+ try {
165
+ const resp = await client.chat.completions.create(req);
166
+ if (stream) {
167
+ let usage = null;
168
+ for await (const chunk of resp) {
169
+ const delta = chunk.choices?.[0]?.delta?.content;
170
+ if (delta) process.stdout.write(delta);
171
+ if (chunk.usage) usage = chunk.usage;
172
+ }
173
+ process.stdout.write('\n');
174
+ const u = fmtUsage(usage);
175
+ if (u) process.stderr.write(`[usage] ${u}\n`);
176
+ } else {
177
+ const choice = resp.choices?.[0] || {};
178
+ if (json) {
179
+ console.log(JSON.stringify(resp, null, 2));
180
+ } else {
181
+ console.log(choice.message?.content ?? '');
182
+ }
183
+ const u = fmtUsage(resp.usage);
184
+ if (u) process.stderr.write(`[usage] ${u}\n`);
185
+ }
186
+ } catch (e) {
187
+ fail(e, { exit: exitOnError });
188
+ }
189
+ }
190
+
191
+ async function repl(client, model, opts) {
192
+ console.error(`[gateway] 交互模式 model=${model},输入文本提问,\`exit\` 退出`);
193
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
194
+ const messages = [];
195
+ const ask = () => new Promise((resolve) => rl.question('你> ', resolve));
196
+ for (;;) {
197
+ let line;
198
+ try {
199
+ line = (await ask()).trim();
200
+ } catch {
201
+ break; // 输入流关闭(Ctrl+D)
202
+ }
203
+ if (!line) continue;
204
+ if (/^(exit|quit)$/i.test(line)) break;
205
+ messages.push({ role: 'user', content: line });
206
+ process.stdout.write('助手> ');
207
+ await chat(client, model, messages, { ...opts, json: false, exitOnError: false });
208
+ }
209
+ rl.close();
210
+ }
211
+
212
+ main();
@@ -0,0 +1,65 @@
1
+ #compdef llm-api-gateway-cli gateway-agent
2
+ # zsh 补全 · cli-agent.js
3
+ #
4
+ # 用法(任选其一):
5
+ # source completions/_llm-api-gateway-cli # 当前 shell 生效
6
+ # 把本文件所在目录加进 $fpath 后 `compinit`(文件名必须是 _llm-api-gateway-cli)
7
+
8
+ _llm_api_gateway_cli() {
9
+ local -a opts
10
+ # 配置键清单必须与 lib/settings.js 的 SETTINGS_SCHEMA 一致(tests/settings.test.mjs 里有契约断言盯着)
11
+ local -a config_keys
12
+ config_keys=(
13
+ model baseUrl temperature maxTokens system mode maxSteps lastWorkDir
14
+ store.dir store.sessionDir retention.days retention.maxTasks
15
+ session.maxBytes history.maxChars history.keepRecentTurns history.readFileDigest
16
+ ui.persist compat.legacyTaskApi
17
+ )
18
+ opts=(
19
+ '(-p --prompt)'{-p,--prompt}'[单轮任务]:文本:'
20
+ '(-i --interactive)'{-i,--interactive}'[多轮交互(REPL)]'
21
+ '(-C --cwd)'{-C,--cwd}'[工作目录]:目录:_files -/'
22
+ '(-m --model)'{-m,--model}'[模型名]:模型:'
23
+ '(-k --key)'{-k,--key}'[网关密钥]:密钥:'
24
+ '--base-url[网关地址]:地址:'
25
+ '(-s --system)'{-s,--system}'[追加的系统提示词]:文本:'
26
+ '--temperature[采样温度]:值:'
27
+ '--max-tokens[最大生成 token 数]:值:'
28
+ '--max-steps[单个任务最多几轮模型调用]:轮数:'
29
+ '--continue[接着最近一次会话继续]'
30
+ '--resume[恢复指定会话]:会话id:'
31
+ '--session-dir[会话落盘目录]:目录:_files -/'
32
+ '--no-session[不落盘会话(纯内存)]'
33
+ '--no-memory[不读取项目记忆]'
34
+ '--allow-bash[开启 bash 工具(默认关闭)]'
35
+ '--output-format[输出格式]:格式:(text stream-json)'
36
+ '--yes[自动批准所有写入(危险)]'
37
+ '--verbose[打印工具结果正文]'
38
+ '--no-color[关闭彩色输出]'
39
+ '(-h --help)'{-h,--help}'[显示帮助]'
40
+ '1:子命令:((config\:查看或修改持久化配置))'
41
+ '*:文件:_files'
42
+ )
43
+ _arguments -s $opts
44
+
45
+ # config 的第二层:list/get/set/unset/path,get/unset/set 后面补键名
46
+ if [[ "$words[2]" == "config" ]]; then
47
+ case "$words[3]" in
48
+ get | unset)
49
+ _describe -t keys '配置键' config_keys
50
+ ;;
51
+ set)
52
+ if (( CURRENT == 4 )); then
53
+ _describe -t keys '配置键' config_keys
54
+ else
55
+ _message '值(字符串带空格就整段写上)'
56
+ fi
57
+ ;;
58
+ '')
59
+ _values '子命令' list get set unset path help
60
+ ;;
61
+ esac
62
+ fi
63
+ }
64
+
65
+ _llm_api_gateway_cli "$@"
@@ -0,0 +1,64 @@
1
+ # bash 补全 · cli-agent.js(命令名 llm-api-gateway-cli / gateway-agent)
2
+ #
3
+ # 用法(任选其一):
4
+ # source completions/llm-api-gateway-cli.bash # 当前 shell 生效
5
+ # cp completions/llm-api-gateway-cli.bash /etc/bash_completion.d/ # 系统级(Linux)
6
+ # cp completions/llm-api-gateway-cli.bash ~/.local/share/bash-completion/completions/llm-api-gateway-cli
7
+
8
+ _llm_api_gateway_cli() {
9
+ local cur prev
10
+ cur="${COMP_WORDS[COMP_CWORD]}"
11
+ prev="${COMP_WORDS[COMP_CWORD-1]}"
12
+
13
+ local opts="--prompt -p --interactive -i --cwd -C --model -m --key -k --base-url --system -s --temperature --max-tokens --max-steps --continue --resume --session-dir --no-session --no-memory --allow-bash --output-format --yes --verbose --no-color --help -h"
14
+ # 配置键清单必须与 lib/settings.js 的 SETTINGS_SCHEMA 一致(tests/settings.test.mjs 里有契约断言盯着)
15
+ local config_keys="model baseUrl temperature maxTokens system mode maxSteps lastWorkDir store.dir store.sessionDir retention.days retention.maxTasks session.maxBytes history.maxChars history.keepRecentTurns history.readFileDigest ui.persist compat.legacyTaskApi"
16
+
17
+ # 子命令 config:补子命令本身、以及 set/get/unset 后面的键名
18
+ if [[ "${COMP_WORDS[1]}" == "config" ]]; then
19
+ case "$prev" in
20
+ config)
21
+ COMPREPLY=($(compgen -W "list get set unset path help" -- "$cur"))
22
+ return
23
+ ;;
24
+ get | unset)
25
+ COMPREPLY=($(compgen -W "$config_keys" -- "$cur"))
26
+ return
27
+ ;;
28
+ set)
29
+ # 第 3 个词是键,第 4 个起是自由文本的值,不再补全
30
+ if [[ "$COMP_CWORD" == 3 ]]; then
31
+ COMPREPLY=($(compgen -W "$config_keys" -- "$cur"))
32
+ fi
33
+ return
34
+ ;;
35
+ list | path)
36
+ return
37
+ ;;
38
+ esac
39
+ fi
40
+
41
+ case "$prev" in
42
+ -C | --cwd | --session-dir)
43
+ COMPREPLY=($(compgen -d -- "$cur"))
44
+ return
45
+ ;;
46
+ --output-format)
47
+ COMPREPLY=($(compgen -W "text stream-json" -- "$cur"))
48
+ return
49
+ ;;
50
+ -p | --prompt | -m | --model | -k | --key | -s | --system | --base-url | --temperature | --max-tokens | --max-steps | --resume)
51
+ return # 需要自由文本,不做补全
52
+ ;;
53
+ esac
54
+
55
+ if [[ "$cur" == -* ]]; then
56
+ COMPREPLY=($(compgen -W "$opts" -- "$cur"))
57
+ elif [[ "$COMP_CWORD" == 1 ]]; then
58
+ COMPREPLY=($(compgen -W "config" -- "$cur"))
59
+ else
60
+ COMPREPLY=($(compgen -f -- "$cur"))
61
+ fi
62
+ }
63
+
64
+ complete -F _llm_api_gateway_cli llm-api-gateway-cli gateway-agent
@@ -0,0 +1,43 @@
1
+ # fish 补全 · cli-agent.js(命令名 llm-api-gateway-cli / gateway-agent)
2
+ #
3
+ # 用法:
4
+ # cp completions/llm-api-gateway-cli.fish ~/.config/fish/completions/
5
+
6
+ set -l cmd llm-api-gateway-cli
7
+
8
+ complete -c $cmd -s p -l prompt -d '单轮任务' -r
9
+ complete -c $cmd -s i -l interactive -d '多轮交互(REPL)'
10
+ complete -c $cmd -s C -l cwd -d '工作目录' -r -a '(__fish_complete_directories)'
11
+ complete -c $cmd -s m -l model -d '模型名' -r
12
+ complete -c $cmd -s k -l key -d '网关密钥' -r
13
+ complete -c $cmd -l base-url -d '网关地址' -r
14
+ complete -c $cmd -s s -l system -d '追加的系统提示词' -r
15
+ complete -c $cmd -l temperature -d '采样温度' -r
16
+ complete -c $cmd -l max-tokens -d '最大生成 token 数' -r
17
+ complete -c $cmd -l max-steps -d '单个任务最多几轮模型调用' -r
18
+ complete -c $cmd -l continue -d '接着最近一次会话继续'
19
+ complete -c $cmd -l resume -d '恢复指定会话' -r
20
+ complete -c $cmd -l session-dir -d '会话落盘目录' -r -a '(__fish_complete_directories)'
21
+ complete -c $cmd -l no-session -d '不落盘会话(纯内存)'
22
+ complete -c $cmd -l no-memory -d '不读取项目记忆'
23
+ complete -c $cmd -l allow-bash -d '开启 bash 工具(默认关闭)'
24
+ complete -c $cmd -l output-format -d '输出格式' -r -a 'text stream-json'
25
+ complete -c $cmd -l yes -d '自动批准所有写入(危险)'
26
+ complete -c $cmd -l verbose -d '打印工具结果正文'
27
+ complete -c $cmd -l no-color -d '关闭彩色输出'
28
+ complete -c $cmd -s h -l help -d '显示帮助'
29
+
30
+ # `config` 子命令:键清单必须与 lib/settings.js 的 SETTINGS_SCHEMA 一致
31
+ # (tests/settings.test.mjs 里有契约断言盯着)
32
+ set -l cfg_keys model baseUrl temperature maxTokens system mode maxSteps lastWorkDir \
33
+ store.dir store.sessionDir retention.days retention.maxTasks \
34
+ session.maxBytes history.maxChars history.keepRecentTurns history.readFileDigest \
35
+ ui.persist compat.legacyTaskApi
36
+
37
+ complete -c $cmd -n '__fish_use_subcommand' -a config -d '查看或修改持久化配置'
38
+ complete -c $cmd -n '__fish_seen_subcommand_from config' -a 'list get set unset path help'
39
+ complete -c $cmd -n '__fish_seen_subcommand_from config; and __fish_seen_subcommand_from get unset' -a "$cfg_keys"
40
+ complete -c $cmd -n '__fish_seen_subcommand_from config; and __fish_seen_subcommand_from set; and test (count (commandline -opc)) -eq 3' -a "$cfg_keys"
41
+
42
+ # gateway-agent 是同一个脚本的别名,共用这套补全
43
+ complete -c gateway-agent -w $cmd
Binary file
Binary file
Binary file