agentsw 0.9.0 → 0.9.2
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 +42 -30
- package/README.zh-CN.md +44 -30
- package/dist/commands.js +37 -15
- package/dist/i18n.js +13 -0
- package/dist/metadata.js +69 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,14 +18,14 @@ without touching the settings you hand-tuned.
|
|
|
18
18
|
</div>
|
|
19
19
|
|
|
20
20
|
```console
|
|
21
|
-
$ asw use
|
|
22
|
-
switching to
|
|
21
|
+
$ asw use myproxy -m glm-5.3-flash
|
|
22
|
+
switching to myproxy (openai) · default model glm-5.3-flash
|
|
23
23
|
|
|
24
24
|
skip claude Claude Code does not support openai-protocol providers
|
|
25
25
|
ok codex ~/.codex/config.toml, ~/.codex/auth.json
|
|
26
26
|
backup: ~/.config/agentsw/backups/transaction-<unique>
|
|
27
27
|
ok omp ~/.omp/agent/models.yml
|
|
28
|
-
select in omp with: omp --model
|
|
28
|
+
select in omp with: omp --model myproxy/glm-5.3-flash
|
|
29
29
|
skip pi pi not detected (pass --apps pi to force)
|
|
30
30
|
ok opencode ~/.config/opencode/opencode.json
|
|
31
31
|
ok dsh ~/.dsh/settings.yaml, ~/.dsh/.credentials.yaml
|
|
@@ -73,35 +73,35 @@ when a portable home directory is desired.
|
|
|
73
73
|
## Quick start
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
|
-
agentsw
|
|
76
|
+
npx agentsw # no install needed — opens the interactive menu
|
|
77
|
+
agentsw # once installed globally; the short alias `asw` is identical
|
|
77
78
|
```
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
**Run it with no arguments and you get a menu — that is the whole tool.** Every action lives
|
|
81
|
+
there: add a provider (auto-detect or manual setup), import the providers already configured in
|
|
82
|
+
your agents, switch provider and default model, re-sync, update a model list, configure model
|
|
83
|
+
metadata, rename or delete, install/upgrade an agent, change language. On the first run it asks
|
|
84
|
+
for English or 简体中文 and offers to adopt what your agents are already using.
|
|
81
85
|
|
|
82
|
-
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
--base-url https://api.example.com/v1 --api-key sk-... --discover
|
|
86
|
+
Each menu action has a command equivalent for scripts, CI and headless machines — see
|
|
87
|
+
[Commands](#commands). Examples below are written as `asw`; without a global install, run them
|
|
88
|
+
as `npx agentsw <command>`.
|
|
86
89
|
|
|
87
|
-
asw
|
|
88
|
-
asw use vfing --dry-run # ...or just show the diffs
|
|
89
|
-
asw status # what each agent points at right now
|
|
90
|
-
```
|
|
90
|
+
One command that needs no menu — `asw status` prints what each agent currently points at:
|
|
91
91
|
|
|
92
92
|
```console
|
|
93
93
|
$ asw status
|
|
94
94
|
config: ~/.config/agentsw/config.json
|
|
95
|
-
active provider:
|
|
96
|
-
|
|
97
|
-
APP
|
|
98
|
-
|
|
99
|
-
claude
|
|
100
|
-
codex
|
|
101
|
-
omp
|
|
102
|
-
pi
|
|
103
|
-
opencode
|
|
104
|
-
dsh
|
|
95
|
+
active provider: myproxy
|
|
96
|
+
|
|
97
|
+
APP FOUND PROTOCOLS CURRENT CONFIG
|
|
98
|
+
-------- ----- ---------------- -------------------------------------- --------------------------------
|
|
99
|
+
claude yes anthropic - ~/.claude/settings.json
|
|
100
|
+
codex yes openai myproxy · glm-5.3-flash ~/.codex/config.toml
|
|
101
|
+
omp yes openai+anthropic providers: sub, sub-anthropic, myproxy ~/.omp/agent/models.yml
|
|
102
|
+
pi no openai+anthropic - ~/.pi/agent/models.json
|
|
103
|
+
opencode yes openai+anthropic myproxy/glm-5.3-flash ~/.config/opencode/opencode.json
|
|
104
|
+
dsh yes openai+anthropic myproxy · glm-5.3-flash ~/.dsh/settings.yaml
|
|
105
105
|
```
|
|
106
106
|
|
|
107
107
|
## Supported apps
|
|
@@ -135,13 +135,14 @@ lists and source apps are unioned. Different protocols on the same host stay sep
|
|
|
135
135
|
|
|
136
136
|
```console
|
|
137
137
|
$ asw import --all
|
|
138
|
+
|
|
138
139
|
ID PROTOCOL BASE URL MODELS FROM KEY
|
|
139
140
|
------------- --------- -------------------------------------- ------ --------- ---
|
|
140
|
-
any anthropic https://a-
|
|
141
|
-
sub openai https://
|
|
142
|
-
sub-anthropic anthropic https://
|
|
141
|
+
any anthropic https://a-long-reseller.example.com... 1 cc-switch yes
|
|
142
|
+
sub openai https://api.example.com/v1 18 omp yes
|
|
143
|
+
sub-anthropic anthropic https://api.example.com 18 omp yes
|
|
143
144
|
zhipu-glm anthropic https://open.bigmodel.cn/api/anthropic 1 cc-switch yes
|
|
144
|
-
imported sub · openai · https://
|
|
145
|
+
imported sub · openai · https://api.example.com/v1 · 18 models [from omp]
|
|
145
146
|
imported zhipu-glm · anthropic · https://open.bigmodel.cn/api/anthropic · 1 models [from cc-switch]
|
|
146
147
|
```
|
|
147
148
|
|
|
@@ -251,7 +252,7 @@ Import still skips already configured accounts.
|
|
|
251
252
|
| `use <id>` | switch every detected agent; `-a codex,omp`, `-m <model>`, `--dry-run` |
|
|
252
253
|
| `sync` | re-apply the active provider (after an agent update, say) |
|
|
253
254
|
| `discover <id> [--sync]` | refresh the model list + metadata from `/v1/models` |
|
|
254
|
-
| `models [query]` | search the models.dev catalog |
|
|
255
|
+
| `models [query]` | search the models.dev catalog (`-r` refresh the cache, `-l <n>` limit rows) |
|
|
255
256
|
| `refresh [--provider <id>]` | refresh saved metadata and optionally configure Gateway; keep the model list |
|
|
256
257
|
| `models --provider <id> --metadata` | inspect field sources, conflicts and reference pricing as JSON |
|
|
257
258
|
| `prune <id>` / `remove <id> [--prune]` | remove from app configs / from the store |
|
|
@@ -259,7 +260,11 @@ Import still skips already configured accounts.
|
|
|
259
260
|
| `apps` / `install <app>` / `upgrade` | agent version manager |
|
|
260
261
|
|
|
261
262
|
```bash
|
|
262
|
-
|
|
263
|
+
# unattended add: discover the provider's model list, then write it everywhere
|
|
264
|
+
asw add -y --id myproxy --protocol openai --openai-api responses \
|
|
265
|
+
--base-url https://api.example.com/v1 --api-key sk-... --discover
|
|
266
|
+
asw use myproxy # every detected agent
|
|
267
|
+
asw use myproxy -a codex,omp -m glm-5.2 # ...or only these, with another default model
|
|
263
268
|
asw discover myproxy --sync
|
|
264
269
|
asw remove myproxy --prune
|
|
265
270
|
```
|
|
@@ -268,6 +273,13 @@ New automatic IDs include the full hostname and protocol, e.g. `api-example-com-
|
|
|
268
273
|
An explicit `--id` is retained, and syncing never renames an existing provider. Import
|
|
269
274
|
deduplication compares endpoint, protocol, and credentials; different accounts remain separate.
|
|
270
275
|
|
|
276
|
+
A bare IP works the same way — every dot and colon becomes a dash, and the **port is not part
|
|
277
|
+
of the ID**, so `http://152.53.108.231:3456` becomes `152-53-108-231-openai`. Two ports on that
|
|
278
|
+
host therefore share a base ID and the second provider gets `-2` appended
|
|
279
|
+
(`152-53-108-231-openai-2`); pass `--id` to name them yourself. IPv6 hosts lose their brackets
|
|
280
|
+
and dashes (`https://[2001:db8::1]:8080` → `2001-db8-1-openai`), an IDN host uses its punycode
|
|
281
|
+
form, and a base URL that cannot be parsed falls back to `imported`.
|
|
282
|
+
|
|
271
283
|
```bash
|
|
272
284
|
asw rename myproxy api-example-com-openai --dry-run
|
|
273
285
|
asw rename myproxy api-example-com-openai
|
package/README.zh-CN.md
CHANGED
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
</div>
|
|
19
19
|
|
|
20
20
|
```console
|
|
21
|
-
$ asw use
|
|
22
|
-
switching to
|
|
21
|
+
$ asw use myproxy -m glm-5.3-flash
|
|
22
|
+
switching to myproxy (openai) · default model glm-5.3-flash
|
|
23
23
|
|
|
24
24
|
skip claude Claude Code does not support openai-protocol providers
|
|
25
25
|
ok codex ~/.codex/config.toml, ~/.codex/auth.json
|
|
26
26
|
backup: ~/.config/agentsw/backups/transaction-<unique>
|
|
27
27
|
ok omp ~/.omp/agent/models.yml
|
|
28
|
-
select in omp with: omp --model
|
|
28
|
+
select in omp with: omp --model myproxy/glm-5.3-flash
|
|
29
29
|
skip pi pi not detected (pass --apps pi to force)
|
|
30
30
|
ok opencode ~/.config/opencode/opencode.json
|
|
31
31
|
ok dsh ~/.dsh/settings.yaml, ~/.dsh/.credentials.yaml
|
|
@@ -71,35 +71,32 @@ npm install -g agentsw # 需要 Node >= 22.13,内置 SQLite 无需额外启
|
|
|
71
71
|
## 快速开始
|
|
72
72
|
|
|
73
73
|
```bash
|
|
74
|
-
agentsw
|
|
74
|
+
npx agentsw # 免安装,直接打开交互菜单
|
|
75
|
+
agentsw # 全局安装后可用;短名 `asw` 完全等价
|
|
75
76
|
```
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
**不带参数运行就是一个菜单——整个工具都在里面。** 添加供应商(自动识别或手动设置)、导入各智能体
|
|
79
|
+
已有的供应商、切换供应商和默认模型、重新同步、更新模型列表、设置模型参数补充源、重命名或删除、
|
|
80
|
+
安装/升级智能体、切换语言。首次运行会先问你要 English 还是简体中文,并提议接管你各智能体已经在用的配置。
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
asw add -y --id vfing --protocol openai --openai-api responses \
|
|
83
|
-
--base-url https://api.example.com/v1 --api-key sk-... --discover
|
|
82
|
+
菜单里的每项操作都有对应的命令,方便脚本、CI 和无头机器——见 [命令](#命令)。
|
|
83
|
+
下文示例统一写作 `asw`;没有全局安装时,请改成 `npx agentsw <命令>`。
|
|
84
84
|
|
|
85
|
-
asw
|
|
86
|
-
asw use vfing --dry-run # ……或者只看 diff
|
|
87
|
-
asw status # 各智能体当前指向什么
|
|
88
|
-
```
|
|
85
|
+
不用菜单也能看一眼——`asw status` 打印各智能体当前指向哪个供应商:
|
|
89
86
|
|
|
90
87
|
```console
|
|
91
88
|
$ asw status
|
|
92
89
|
config: ~/.config/agentsw/config.json
|
|
93
|
-
active provider:
|
|
94
|
-
|
|
95
|
-
APP
|
|
96
|
-
|
|
97
|
-
claude
|
|
98
|
-
codex
|
|
99
|
-
omp
|
|
100
|
-
pi
|
|
101
|
-
opencode
|
|
102
|
-
dsh
|
|
90
|
+
active provider: myproxy
|
|
91
|
+
|
|
92
|
+
APP FOUND PROTOCOLS CURRENT CONFIG
|
|
93
|
+
-------- ----- ---------------- -------------------------------------- --------------------------------
|
|
94
|
+
claude yes anthropic - ~/.claude/settings.json
|
|
95
|
+
codex yes openai myproxy · glm-5.3-flash ~/.codex/config.toml
|
|
96
|
+
omp yes openai+anthropic providers: sub, sub-anthropic, myproxy ~/.omp/agent/models.yml
|
|
97
|
+
pi no openai+anthropic - ~/.pi/agent/models.json
|
|
98
|
+
opencode yes openai+anthropic myproxy/glm-5.3-flash ~/.config/opencode/opencode.json
|
|
99
|
+
dsh yes openai+anthropic myproxy · glm-5.3-flash ~/.dsh/settings.yaml
|
|
103
100
|
```
|
|
104
101
|
|
|
105
102
|
## 支持的应用
|
|
@@ -119,6 +116,11 @@ dsh yes openai+anthropic vfing · glm-5.3-flash ~/.ds
|
|
|
119
116
|
未安装的应用会被跳过,而不是瞎猜。`--apps codex,omp` 只跑指定应用;
|
|
120
117
|
对未检测到的应用显式传 `--apps` 可强制写入。
|
|
121
118
|
|
|
119
|
+
交互菜单写配置前会问同一个问题:多选框第一行是**全部检测到的应用**,已检测到的排在前面,
|
|
120
|
+
未检测到的会标注出来。上次的选择会被记住,所以菜单里的 `use`/`sync` 只会继续写你勾选的那几个。
|
|
121
|
+
任何配置文件解析失败(JSON/YAML/TOML 非法)都会报出文件与位置,绝不回显出错那一行;
|
|
122
|
+
`--dry-run` 同样保留这些细节,而不是含糊带过。
|
|
123
|
+
|
|
122
124
|
## 导入你已有的配置
|
|
123
125
|
|
|
124
126
|
`import` 读取每个智能体的配置**以及 cc-switch 的 SQLite 存储**(只读),解析内联或环境变量
|
|
@@ -127,13 +129,14 @@ dsh yes openai+anthropic vfing · glm-5.3-flash ~/.ds
|
|
|
127
129
|
|
|
128
130
|
```console
|
|
129
131
|
$ asw import --all
|
|
132
|
+
|
|
130
133
|
ID PROTOCOL BASE URL MODELS FROM KEY
|
|
131
134
|
------------- --------- -------------------------------------- ------ --------- ---
|
|
132
|
-
any anthropic https://a-
|
|
133
|
-
sub openai https://
|
|
134
|
-
sub-anthropic anthropic https://
|
|
135
|
+
any anthropic https://a-long-reseller.example.com... 1 cc-switch yes
|
|
136
|
+
sub openai https://api.example.com/v1 18 omp yes
|
|
137
|
+
sub-anthropic anthropic https://api.example.com 18 omp yes
|
|
135
138
|
zhipu-glm anthropic https://open.bigmodel.cn/api/anthropic 1 cc-switch yes
|
|
136
|
-
imported sub · openai · https://
|
|
139
|
+
imported sub · openai · https://api.example.com/v1 · 18 models [from omp]
|
|
137
140
|
imported zhipu-glm · anthropic · https://open.bigmodel.cn/api/anthropic · 1 models [from cc-switch]
|
|
138
141
|
```
|
|
139
142
|
|
|
@@ -234,7 +237,7 @@ asw sync --provider myproxy
|
|
|
234
237
|
| `use <id>` | 切换所有检测到的智能体;`-a codex,omp`、`-m <model>`、`--dry-run` |
|
|
235
238
|
| `sync` | 重新应用当前供应商(比如某个智能体升级之后) |
|
|
236
239
|
| `discover <id> [--sync]` | 从 `/v1/models` 刷新模型列表与元数据 |
|
|
237
|
-
| `models [query]` | 搜索 models.dev
|
|
240
|
+
| `models [query]` | 搜索 models.dev 目录(`-r` 强制刷新缓存,`-l <n>` 限制条数) |
|
|
238
241
|
| `refresh [--provider <id>]` | 刷新已有模型参数,可设置 Gateway 补充源,不改变模型列表 |
|
|
239
242
|
| `models --provider <id> --metadata` | 以 JSON 查看字段来源、冲突和参考价格 |
|
|
240
243
|
| `prune <id>` / `remove <id> [--prune]` | 从各应用配置中清除 / 从存储中删除 |
|
|
@@ -242,7 +245,11 @@ asw sync --provider myproxy
|
|
|
242
245
|
| `apps` / `install <app>` / `upgrade` | 智能体版本管理 |
|
|
243
246
|
|
|
244
247
|
```bash
|
|
245
|
-
|
|
248
|
+
# 无人值守:添加供应商并自动发现模型列表,然后写入所有智能体
|
|
249
|
+
asw add -y --id myproxy --protocol openai --openai-api responses \
|
|
250
|
+
--base-url https://api.example.com/v1 --api-key sk-... --discover
|
|
251
|
+
asw use myproxy # 所有检测到的智能体
|
|
252
|
+
asw use myproxy -a codex,omp -m glm-5.2 # ……或只写这几个,并换一个默认模型
|
|
246
253
|
asw discover myproxy --sync
|
|
247
254
|
asw remove myproxy --prune
|
|
248
255
|
```
|
|
@@ -251,6 +258,13 @@ asw remove myproxy --prune
|
|
|
251
258
|
显式指定的 `--id` 会保留,同步不会自动重命名。导入去重会同时比较端点、协议和凭据,
|
|
252
259
|
不同账号保持独立;同一账号的自定义名称优先于自动生成名称。
|
|
253
260
|
|
|
261
|
+
纯 IP 的填写方式完全一样——域名里的每个点和冒号都变成短横线,**端口不属于 ID**:
|
|
262
|
+
`http://152.53.108.231:3456` 生成的 ID 是 `152-53-108-231-openai`。因此同一 IP 上的两个端口
|
|
263
|
+
共用一个基础 ID,第二个供应商会自动带上 `-2`(`152-53-108-231-openai-2`);
|
|
264
|
+
想区分请自己传 `--id`。IPv6 会去掉方括号并把冒号变短横线
|
|
265
|
+
(`https://[2001:db8::1]:8080` → `2001-db8-1-openai`),国际化域名使用 punycode,
|
|
266
|
+
无法解析的 base URL 退回 `imported`。
|
|
267
|
+
|
|
254
268
|
```bash
|
|
255
269
|
asw rename myproxy api-example-com-openai --dry-run
|
|
256
270
|
asw rename myproxy api-example-com-openai
|
package/dist/commands.js
CHANGED
|
@@ -8,7 +8,7 @@ import prompts from "prompts";
|
|
|
8
8
|
import { loadStore, saveStore, getProvider, configFile } from "./store.js";
|
|
9
9
|
import { scanCandidates, normalizeUrl, findMatchingProvider } from "./import.js";
|
|
10
10
|
import { loadCatalog, searchCatalog } from "./modelsdev.js";
|
|
11
|
-
import { enrichProviderModels, getMetadataMode, resolveMetadataOptions } from "./metadata.js";
|
|
11
|
+
import { classifyUnresolved, enrichProviderModels, getMetadataMode, resolveMetadataOptions } from "./metadata.js";
|
|
12
12
|
import { loadGatewayCatalog } from "./gateway.js";
|
|
13
13
|
import { resolveTargets, supportsProtocol, targets } from "./targets/index.js";
|
|
14
14
|
import { discoverProviderModels, probeProtocols } from "./discover.js";
|
|
@@ -70,11 +70,19 @@ function modelRows(models) {
|
|
|
70
70
|
]);
|
|
71
71
|
}
|
|
72
72
|
const MODEL_HEADER = ["MODEL", "CTX", "IN", "OUT", "REASONING", "$IN/$OUT per M"];
|
|
73
|
-
/**
|
|
74
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Explain why some models stayed bare: only ambiguous ids are worth the user's
|
|
75
|
+
* attention, so those are named separately from ids nothing can resolve.
|
|
76
|
+
*/
|
|
77
|
+
function printUncatalogedHint(models, catalog, hint) {
|
|
75
78
|
const bare = models.filter((m) => m.contextWindow === undefined && !m.cost);
|
|
76
|
-
if (bare.length
|
|
77
|
-
|
|
79
|
+
if (bare.length === 0)
|
|
80
|
+
return;
|
|
81
|
+
console.log(pc.dim(t("meta.gap", { count: bare.length })));
|
|
82
|
+
const { ambiguous, unknown, noCreatorRow } = classifyUnresolved(catalog, bare.map((m) => m.id), hint);
|
|
83
|
+
for (const [key, ids] of [["meta.ambiguous", ambiguous], ["meta.noCreatorRow", noCreatorRow], ["meta.unknown", unknown]]) {
|
|
84
|
+
if (ids.length)
|
|
85
|
+
console.log(pc.dim(` ${t(key, { count: ids.length, ids: ids.join(", "), id: ids[0] })}`));
|
|
78
86
|
}
|
|
79
87
|
}
|
|
80
88
|
/** Guess the models.dev provider whose API host matches the configured baseUrl. */
|
|
@@ -197,8 +205,15 @@ export async function cmdAdd(opts) {
|
|
|
197
205
|
fail(t("add.openaiApiInvalid"));
|
|
198
206
|
const baseUrl = normalizeUrl(answers.baseUrl);
|
|
199
207
|
const explicitId = answers.id || undefined;
|
|
200
|
-
const
|
|
201
|
-
|
|
208
|
+
const byId = explicitId ? store.providers[explicitId] : undefined;
|
|
209
|
+
// Endpoint plus credential is the account identity, so an explicit id never
|
|
210
|
+
// licenses a second entry for an account that is already configured.
|
|
211
|
+
const sameAccount = byId ? undefined : findMatchingProvider(Object.values(store.providers), { protocol, baseUrl, apiKey: answers.apiKey });
|
|
212
|
+
const existing = byId ?? sameAccount;
|
|
213
|
+
if (byId === undefined && explicitId && sameAccount) {
|
|
214
|
+
console.log(pc.yellow(t("add.alreadyConfigured", { id: pc.bold(sameAccount.id) })));
|
|
215
|
+
}
|
|
216
|
+
const id = byId?.id ?? sameAccount?.id ?? explicitId ?? availableProviderId(providerIdFromBaseUrl(baseUrl, protocol), store.providers);
|
|
202
217
|
const openaiApi = protocol === "openai" ? wire ?? existing?.openaiApi : undefined;
|
|
203
218
|
const metadataOptions = resolveMetadataOptions(opts, existing);
|
|
204
219
|
const manualIds = (answers.models ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -256,7 +271,7 @@ export async function cmdAdd(opts) {
|
|
|
256
271
|
console.log(pc.green(t("add.saved", { status: savedStatus, id: pc.bold(provider.id), protocol })));
|
|
257
272
|
console.log(t("add.metadata", { matched, total: models.length }) + (hint ? t("add.providerHint", { hint }) : ""));
|
|
258
273
|
console.log(table(modelRows(models), MODEL_HEADER));
|
|
259
|
-
printUncatalogedHint(models);
|
|
274
|
+
printUncatalogedHint(models, catalog, hint);
|
|
260
275
|
console.log(pc.dim(`\n${t("add.next", { id: provider.id })}`));
|
|
261
276
|
}
|
|
262
277
|
/** Shared logic to create and save a single provider from discovered models. */
|
|
@@ -297,7 +312,7 @@ async function createProvider(opts) {
|
|
|
297
312
|
console.log(pc.green(t("add.saved", { status: savedStatus, id: pc.bold(provider.id), protocol })));
|
|
298
313
|
console.log(t("add.metadata", { matched, total: models.length }) + (hint ? t("add.providerHint", { hint }) : ""));
|
|
299
314
|
console.log(table(modelRows(models), MODEL_HEADER));
|
|
300
|
-
printUncatalogedHint(models);
|
|
315
|
+
printUncatalogedHint(models, catalog, hint);
|
|
301
316
|
return provider;
|
|
302
317
|
}
|
|
303
318
|
/**
|
|
@@ -339,8 +354,13 @@ export async function cmdQuickAdd(opts) {
|
|
|
339
354
|
const gatewayLoader = sharedGatewayLoader();
|
|
340
355
|
for (const protocol of protocols) {
|
|
341
356
|
const explicitId = opts.id === undefined ? undefined : `${opts.id}${multi ? `-${protocol}` : ""}`;
|
|
342
|
-
const
|
|
343
|
-
const
|
|
357
|
+
const byId = explicitId ? store.providers[explicitId] : undefined;
|
|
358
|
+
const sameAccount = byId ? undefined : findMatchingProvider(Object.values(store.providers), { protocol, baseUrl, apiKey });
|
|
359
|
+
const existing = byId ?? sameAccount;
|
|
360
|
+
if (byId === undefined && explicitId && sameAccount) {
|
|
361
|
+
console.log(pc.yellow(t("add.alreadyConfigured", { id: pc.bold(sameAccount.id) })));
|
|
362
|
+
}
|
|
363
|
+
const id = byId?.id ?? sameAccount?.id ?? explicitId ?? availableProviderId(providerIdFromBaseUrl(baseUrl, protocol), store.providers);
|
|
344
364
|
const name = opts.name ?? existing?.name ?? (opts.id === undefined ? providerNameFromBaseUrl(baseUrl, protocol) : multi ? `${opts.id} (${protocol})` : opts.id);
|
|
345
365
|
const modelFilter = parseFilterOpts(opts, existing?.modelFilter);
|
|
346
366
|
process.stderr.write(pc.dim(`${t("add.discovering", { url: baseUrl })} [${protocol}]\n`));
|
|
@@ -641,7 +661,7 @@ export async function cmdModels(query, opts) {
|
|
|
641
661
|
}
|
|
642
662
|
console.log(`${pc.bold(provider.id)} (${provider.protocol}) · ${provider.baseUrl}`);
|
|
643
663
|
console.log(table(modelRows(provider.models), MODEL_HEADER));
|
|
644
|
-
printUncatalogedHint(provider.models);
|
|
664
|
+
printUncatalogedHint(provider.models, await loadCatalog(), provider.modelsDevId);
|
|
645
665
|
return;
|
|
646
666
|
}
|
|
647
667
|
const catalog = await loadCatalog({ refresh: opts.refresh });
|
|
@@ -680,9 +700,11 @@ export async function cmdRefreshMeta(opts = {}) {
|
|
|
680
700
|
updated++;
|
|
681
701
|
}
|
|
682
702
|
saveStore(store);
|
|
683
|
-
console.log(pc.green(
|
|
703
|
+
console.log(pc.green(t("refresh.checked", { changed: updated })));
|
|
684
704
|
if (updated > 0)
|
|
685
|
-
console.log(pc.dim("
|
|
705
|
+
console.log(pc.dim(t("refresh.next")));
|
|
706
|
+
for (const provider of providers)
|
|
707
|
+
printUncatalogedHint(provider.models, catalog, provider.modelsDevId);
|
|
686
708
|
}
|
|
687
709
|
export async function cmdDiscover(id, opts) {
|
|
688
710
|
resolveTargets(opts.apps);
|
|
@@ -715,7 +737,7 @@ export async function cmdDiscover(id, opts) {
|
|
|
715
737
|
(added.length ? `\n new: ${added.join(", ")}` : "") +
|
|
716
738
|
(gone.length ? `\n removed upstream: ${gone.join(", ")}` : ""));
|
|
717
739
|
console.log(table(modelRows(provider.models), MODEL_HEADER));
|
|
718
|
-
printUncatalogedHint(provider.models);
|
|
740
|
+
printUncatalogedHint(provider.models, catalog, provider.modelsDevId);
|
|
719
741
|
if (opts.sync) {
|
|
720
742
|
console.log("");
|
|
721
743
|
await cmdSync({ provider: id, apps: opts.apps });
|
package/dist/i18n.js
CHANGED
|
@@ -190,6 +190,7 @@ const messages = {
|
|
|
190
190
|
"add.defaultMissing": { en: "default model {model} is not in the model list", "zh-CN": "默认模型 {model} 不在模型列表中" },
|
|
191
191
|
"add.smallMissing": { en: "small model {model} is not in the model list", "zh-CN": "小型模型 {model} 不在模型列表中" },
|
|
192
192
|
"add.added": { en: "added", "zh-CN": "已添加" },
|
|
193
|
+
"add.alreadyConfigured": { en: "same endpoint and key are already configured as {id}; updating it instead of creating a duplicate", "zh-CN": "相同地址与密钥已配置为 {id};将更新该供应商,不会新建重复项" },
|
|
193
194
|
"add.updated": { en: "updated", "zh-CN": "已更新" },
|
|
194
195
|
"add.saved": { en: "{status} provider {id} ({protocol})", "zh-CN": "{status}供应商 {id} ({protocol})" },
|
|
195
196
|
"add.metadata": { en: "model metadata: {matched}/{total} models have a context limit", "zh-CN": "模型参数:{matched}/{total} 个模型已有上下文上限" },
|
|
@@ -245,6 +246,18 @@ const messages = {
|
|
|
245
246
|
"status.config": { en: "config: {file}", "zh-CN": "配置:{file}" },
|
|
246
247
|
"status.active": { en: "active provider: {id}", "zh-CN": "当前供应商:{id}" },
|
|
247
248
|
"status.none": { en: "(none)", "zh-CN": "(无)" },
|
|
249
|
+
"meta.gap": { en: "{count} model(s) have no catalog metadata (shown as \"-\")", "zh-CN": "{count} 个模型没有目录元数据(显示为 \"-\")" },
|
|
250
|
+
"meta.ambiguous": {
|
|
251
|
+
en: "ambiguous ({count}): {ids} — several creators list these; pin with --gateway-models '{\"{id}\":\"creator/model\"}'",
|
|
252
|
+
"zh-CN": "存在歧义({count}):{ids} —— 多个厂商都提供;用 --gateway-models '{\"{id}\":\"creator/model\"}' 指定",
|
|
253
|
+
},
|
|
254
|
+
"meta.unknown": { en: "not listed by any creator ({count}): {ids}", "zh-CN": "所有厂商均未收录({count}):{ids}" },
|
|
255
|
+
"meta.noCreatorRow": {
|
|
256
|
+
en: "no models.dev row for the creator ({count}): {ids} — filled from AI Gateway when available",
|
|
257
|
+
"zh-CN": "厂商在 models.dev 无对应条目({count}):{ids} —— 有 AI Gateway 数据时会自动补全",
|
|
258
|
+
},
|
|
259
|
+
"refresh.checked": { en: "checked model metadata ({changed} provider(s) changed)", "zh-CN": "已检查模型元数据({changed} 个供应商有变化)" },
|
|
260
|
+
"refresh.next": { en: "run `agentsw sync` to push updated metadata into app configs", "zh-CN": "运行 `agentsw sync` 将更新的元数据写入各智能体配置" },
|
|
248
261
|
};
|
|
249
262
|
let current = detectSystemLocale();
|
|
250
263
|
export function normalizeLocale(value) {
|
package/dist/metadata.js
CHANGED
|
@@ -105,25 +105,42 @@ function equalValue(a, b) {
|
|
|
105
105
|
function indexCatalog(catalog) {
|
|
106
106
|
const exact = new Map();
|
|
107
107
|
const canonical = new Map();
|
|
108
|
+
const identities = new Map();
|
|
109
|
+
const push = (map, key, entry) => {
|
|
110
|
+
const matches = map.get(key);
|
|
111
|
+
if (matches)
|
|
112
|
+
matches.push(entry);
|
|
113
|
+
else
|
|
114
|
+
map.set(key, [entry]);
|
|
115
|
+
};
|
|
108
116
|
for (const provider of Object.values(catalog)) {
|
|
109
117
|
for (const [key, model] of Object.entries(provider.models)) {
|
|
110
118
|
const modelId = model.id.includes("/") ? model.id : `${provider.id}/${model.id}`;
|
|
111
|
-
const entry = { provider: provider.id, modelId, model };
|
|
112
|
-
for (const id of new Set([key, model.id]))
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
else
|
|
117
|
-
exact.set(id, [entry]);
|
|
118
|
-
}
|
|
119
|
-
const matches = canonical.get(modelId);
|
|
120
|
-
if (matches)
|
|
121
|
-
matches.push(entry);
|
|
122
|
-
else
|
|
123
|
-
canonical.set(modelId, [entry]);
|
|
119
|
+
const entry = { provider: provider.id, modelId, identity: model.canonical_model_id ?? modelId, model };
|
|
120
|
+
for (const id of new Set([key, model.id]))
|
|
121
|
+
push(exact, id, entry);
|
|
122
|
+
push(canonical, modelId, entry);
|
|
123
|
+
push(identities, entry.identity, entry);
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
|
-
return { exact, canonical };
|
|
126
|
+
return { exact, canonical, identities };
|
|
127
|
+
}
|
|
128
|
+
/** Creator identity of a catalog model id, or undefined when its rows disagree. */
|
|
129
|
+
function catalogIdentity(index, modelId) {
|
|
130
|
+
const identities = new Set(index?.canonical.get(modelId)?.map((entry) => entry.identity));
|
|
131
|
+
return identities?.size === 1 ? [...identities][0] : undefined;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The creator's own listing for an identity, when models.dev carries one.
|
|
135
|
+
* Gateways also key their rows by the canonical id, and those rows carry the
|
|
136
|
+
* gateway's own limits and prices, so only a row from the creator's own
|
|
137
|
+
* namespace counts as authoritative.
|
|
138
|
+
*/
|
|
139
|
+
function creatorEntry(index, identity) {
|
|
140
|
+
const slash = identity.indexOf("/");
|
|
141
|
+
if (slash < 1)
|
|
142
|
+
return undefined;
|
|
143
|
+
return index?.identities.get(identity)?.find((entry) => entry.modelId === identity && entry.provider === identity.slice(0, slash));
|
|
127
144
|
}
|
|
128
145
|
function indexGatewayBareIds(gateway) {
|
|
129
146
|
const result = new Map();
|
|
@@ -147,10 +164,15 @@ function selectExact(index, id, hint, aliases, gateway, gatewayBareIds) {
|
|
|
147
164
|
const candidate = gatewayBareIds?.get(id);
|
|
148
165
|
const evidence = index?.exact.get(id);
|
|
149
166
|
const hintedEvidence = hint ? evidence?.filter((entry) => entry.provider === hint) : undefined;
|
|
150
|
-
const identities = new Set((hintedEvidence?.length ? hintedEvidence : evidence)?.map((entry) => entry.
|
|
167
|
+
const identities = new Set((hintedEvidence?.length ? hintedEvidence : evidence)?.map((entry) => entry.identity));
|
|
168
|
+
// Gateway names the creator while models.dev names the listing provider, so
|
|
169
|
+
// the two agree only once both are reduced to a creator identity. A gateway
|
|
170
|
+
// id the catalog has no row for stands for itself.
|
|
171
|
+
const candidateIdentity = candidate ? catalogIdentity(index, candidate) ?? candidate : undefined;
|
|
151
172
|
// A unique exact basename is safe only with absent or agreeing primary evidence.
|
|
152
173
|
// Explicit provider hints can resolve primary ambiguity, but never Gateway ambiguity.
|
|
153
|
-
if (candidate &&
|
|
174
|
+
if (candidate && candidateIdentity !== undefined
|
|
175
|
+
&& (identities.size === 0 || (identities.size === 1 && identities.has(candidateIdentity))))
|
|
154
176
|
gatewayId = candidate;
|
|
155
177
|
}
|
|
156
178
|
}
|
|
@@ -159,10 +181,10 @@ function selectExact(index, id, hint, aliases, gateway, gatewayBareIds) {
|
|
|
159
181
|
: (bare && hint ? index?.canonical.get(`${hint}/${id}`) : undefined)
|
|
160
182
|
?? index?.canonical.get(id) ?? index?.exact.get(id);
|
|
161
183
|
const hinted = candidates?.find((entry) => entry.provider === hint);
|
|
162
|
-
const identities = new Set(candidates?.map((entry) => entry.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
184
|
+
const identities = new Set(candidates?.map((entry) => entry.identity));
|
|
185
|
+
// Reseller rows for one model share an identity but disagree on limits and
|
|
186
|
+
// prices, so only the creator's own row is authoritative.
|
|
187
|
+
const primary = hinted ?? (identities.size === 1 ? creatorEntry(index, [...identities][0]) : undefined);
|
|
166
188
|
return { primary, gatewayId, alias };
|
|
167
189
|
}
|
|
168
190
|
function legacyPrimary(catalog, id, hint) {
|
|
@@ -398,3 +420,30 @@ export async function enrichProviderModels(catalog, ids, settings, options = {})
|
|
|
398
420
|
// bounds must be resolved with the supplement against the original values.
|
|
399
421
|
return gateway ? enrich(gateway) : primaryOnly ?? enrich();
|
|
400
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Why a model id stayed bare after enrichment, so callers can tell the user
|
|
425
|
+
* what is worth acting on: "ambiguous" ids are listed under more than one
|
|
426
|
+
* creator and an explicit mapping resolves them; "unknown" ids no creator
|
|
427
|
+
* lists at all, so no mapping helps; "noCreatorRow" ids have one unambiguous
|
|
428
|
+
* creator but models.dev carries no authoritative row for it, so only Gateway
|
|
429
|
+
* can fill them.
|
|
430
|
+
*/
|
|
431
|
+
export function classifyUnresolved(catalog, ids, hint) {
|
|
432
|
+
const ambiguous = [];
|
|
433
|
+
const unknown = [];
|
|
434
|
+
const noCreatorRow = [];
|
|
435
|
+
if (!catalog)
|
|
436
|
+
return { ambiguous, unknown: [...ids], noCreatorRow };
|
|
437
|
+
const index = indexCatalog(catalog);
|
|
438
|
+
for (const id of ids) {
|
|
439
|
+
const hinted = hint ? index.canonical.get(`${hint}/${id}`) : undefined;
|
|
440
|
+
const candidates = hinted ?? index.canonical.get(id) ?? index.exact.get(id);
|
|
441
|
+
if (!candidates?.length)
|
|
442
|
+
unknown.push(id);
|
|
443
|
+
else if (new Set(candidates.map((entry) => entry.identity)).size > 1)
|
|
444
|
+
ambiguous.push(id);
|
|
445
|
+
else if (!creatorEntry(index, [...new Set(candidates.map((entry) => entry.identity))][0]))
|
|
446
|
+
noCreatorRow.push(id);
|
|
447
|
+
}
|
|
448
|
+
return { ambiguous, unknown, noCreatorRow };
|
|
449
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentsw",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "CLI provider switcher for coding agents: manage OpenAI/Anthropic-protocol providers in one place and sync them, with models.dev metadata, into Claude Code, Codex, omp, pi, prime-agent, opencode, Hermes, WorkBuddy and DeepSeek Harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|