agentsw 0.10.2 → 0.11.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 +44 -12
- package/README.zh-CN.md +38 -10
- package/dist/commands.js +262 -54
- package/dist/i18n.js +29 -13
- package/dist/index.js +10 -3
- package/dist/menu.js +39 -8
- package/dist/targets/claudecode.js +2 -0
- package/dist/targets/dsh.js +3 -2
- package/dist/targets/omp.js +4 -21
- package/dist/targets/pistyle.js +10 -28
- package/dist/targets/wire.js +65 -0
- package/dist/targets/workbuddy.js +7 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,8 @@ without touching the settings you hand-tuned.
|
|
|
27
27
|
`npx agentsw use myproxy -a codex,omp` is one line in a provisioning script; nine GUI
|
|
28
28
|
clicks are not.
|
|
29
29
|
- **`--dry-run` prints redacted diffs.** Preview changes without writing files or creating
|
|
30
|
-
backups
|
|
30
|
+
backups — the provider store included, so a previewed `sync` does not leave its refreshed
|
|
31
|
+
model list behind. Existing configuration files are backed up when changes are committed.
|
|
31
32
|
- **Non-destructive by design.** Only the fields agentsw owns are rewritten. Provider-level
|
|
32
33
|
keys it does not model — `authHeader`, `headers`, `compat`, `discovery` — per-model extras
|
|
33
34
|
like `thinkingLevelMap`, and YAML comments all survive a re-sync.
|
|
@@ -94,9 +95,10 @@ agentsw # once installed globally; the short alias `asw` is id
|
|
|
94
95
|
|
|
95
96
|
**Run it with no arguments and you get a menu — that is the whole tool.** Every action lives
|
|
96
97
|
there: add a provider (auto-detect or manual setup), import the providers already configured in
|
|
97
|
-
your agents, switch provider and default model,
|
|
98
|
-
metadata, rename or delete, install/upgrade an
|
|
99
|
-
for English or 简体中文 and offers to adopt what
|
|
98
|
+
your agents, switch provider and default model, sync a provider (fetch its current model list,
|
|
99
|
+
then write it to your agents), configure model metadata, rename or delete, install/upgrade an
|
|
100
|
+
agent, change language. On the first run it asks for English or 简体中文 and offers to adopt what
|
|
101
|
+
your agents are already using.
|
|
100
102
|
|
|
101
103
|
Each menu action has a command equivalent for scripts, CI and headless machines — see
|
|
102
104
|
[Commands](#commands). Examples below are written as `asw`; without a global install, run them
|
|
@@ -213,6 +215,10 @@ asw add -y --discover --exclude "*embedding*,*video*" --id myproxy ...
|
|
|
213
215
|
asw add -y --discover --no-dedup ... # keep duplicates
|
|
214
216
|
asw discover myproxy --include "gpt-*,glm-*" # update the persisted filter
|
|
215
217
|
asw discover myproxy --no-filter # clear it
|
|
218
|
+
|
|
219
|
+
# Refresh several providers' model lists without writing any agent config.
|
|
220
|
+
asw discover --all
|
|
221
|
+
asw discover myproxy backup-proxy
|
|
216
222
|
```
|
|
217
223
|
|
|
218
224
|
Explicit `--models` entries and the default model are never filtered out.
|
|
@@ -249,9 +255,16 @@ asw refresh --provider myproxy --metadata-mode off
|
|
|
249
255
|
# Return an explicitly disabled provider to automatic mode
|
|
250
256
|
asw refresh --provider myproxy --metadata-mode auto
|
|
251
257
|
|
|
252
|
-
# Preview and apply separately
|
|
258
|
+
# Preview and apply separately. Sync fetches the model list first;
|
|
259
|
+
# --dry-run still fetches, so the preview matches a real sync, but saves nothing;
|
|
260
|
+
# --no-refresh writes the saved list without touching the network.
|
|
253
261
|
asw sync --provider myproxy --dry-run
|
|
254
262
|
asw sync --provider myproxy
|
|
263
|
+
asw sync --provider myproxy --no-refresh
|
|
264
|
+
|
|
265
|
+
# Refresh and push several providers in one run.
|
|
266
|
+
asw sync --all
|
|
267
|
+
asw sync --provider myproxy,backup-proxy
|
|
255
268
|
```
|
|
256
269
|
|
|
257
270
|
The menu's **Configure and refresh model metadata** action offers **Automatic (recommended)**,
|
|
@@ -286,9 +299,28 @@ Import still skips already configured accounts.
|
|
|
286
299
|
Gateway has a separate 24-hour cache; a failed request is retried once before falling back to stale cached data
|
|
287
300
|
(or skipping supplementation when there is none). `AGENTSW_DEBUG=1` reports why an attempt failed, and
|
|
288
301
|
`AGENTSW_GATEWAY_TIMEOUT_MS` overrides the 15-second whole-request budget.
|
|
289
|
-
- **
|
|
290
|
-
|
|
291
|
-
|
|
302
|
+
- **A model the endpoint dropped is reported, not hidden.** A refresh keeps the default and small
|
|
303
|
+
model even when the listing stops carrying them — one flaky listing must not delete the model you
|
|
304
|
+
chose — but being kept also keeps them out of the "removed upstream" line, so they are named in a
|
|
305
|
+
`warning:` after the apply and in `--json` as `refreshed.missing`. That is what a renamed model
|
|
306
|
+
looks like from the client side: the agents keep working with an id the endpoint now rejects.
|
|
307
|
+
- **One selection everywhere:** `--all` and a comma-separated `-p a,b` mean the same thing on `sync`, `discover`
|
|
308
|
+
and `refresh`, and `discover` also takes its ids positionally (`discover alpha beta`). Every id is resolved before
|
|
309
|
+
the first request, so a typo fails before half the run is on disk. Omitting the selection keeps each command's own
|
|
310
|
+
default: the active provider for `sync` and `discover`, every provider for `refresh`.
|
|
311
|
+
- **Syncing several providers:** `sync --all` takes every configured provider and `-p a,b` takes a list; each is
|
|
312
|
+
refreshed and written in turn, finishing with the active provider so whatever "current" pointer an app keeps ends
|
|
313
|
+
up where the store says it should. An app that names one provider instead of a map — Claude Code's env block — is
|
|
314
|
+
written once rather than once per provider: it gets the active provider when it can accept it, or the single
|
|
315
|
+
provider in the run that it can (Claude Code takes anthropic endpoints only, so an openai active provider does not
|
|
316
|
+
starve it). With several candidates and the active provider not among them the choice is yours, so the app keeps
|
|
317
|
+
pointing where it already does: syncing is not switching. A provider whose endpoint cannot be reached is reported
|
|
318
|
+
and skipped, the rest still sync, and the run exits non-zero.
|
|
319
|
+
- **Automatic lookup is not an automatic push:** enrichment updates saved metadata during add/quick/discover/import/refresh
|
|
320
|
+
and never writes agent configs by itself. `sync` is the push, and it refreshes first: it fetches the provider's model
|
|
321
|
+
list, re-enriches it from the catalogs, saves the store, then applies it. `sync --dry-run` fetches the same list so
|
|
322
|
+
the preview is accurate but saves neither it nor the configs. `sync --no-refresh` writes the saved list without
|
|
323
|
+
fetching, and `discover` stops after the refresh without writing any agent.
|
|
292
324
|
|
|
293
325
|
## Commands
|
|
294
326
|
|
|
@@ -301,11 +333,11 @@ Import still skips already configured accounts.
|
|
|
301
333
|
| `list` / `status` | configured providers / what each agent points at |
|
|
302
334
|
| `list --apps omp,prime` | list agent-local provider IDs, including entries absent from agentsw |
|
|
303
335
|
| `rename <id> <new-id>` | back up and migrate the ID and config references; supports `--dry-run` |
|
|
304
|
-
| `use <id>` | switch every detected agent; `-a codex,omp`, `-m <model>`, `--dry-run` |
|
|
305
|
-
| `sync` | re-apply the
|
|
306
|
-
| `discover
|
|
336
|
+
| `use <id>` | switch every detected agent; `-a codex,omp`, `-m <model>`, `--small-model <id>` / `--no-small-model`, `--dry-run` |
|
|
337
|
+
| `sync` | refresh each provider's model list, then re-apply it to the agent configs; `--all` or `-p a,b` for several, `--no-refresh` writes the saved list |
|
|
338
|
+
| `discover [ids...] [--sync]` | refresh the model list + metadata from `/v1/models`; `--all` or several ids for more than one, no id for the active provider; `--sync` pushes them to the agents |
|
|
307
339
|
| `models [query]` | search the models.dev catalog (`-r` refresh the cache, `-l <n>` limit rows) |
|
|
308
|
-
| `refresh [--provider <
|
|
340
|
+
| `refresh [--provider <ids>]` | refresh saved metadata and optionally configure Gateway; keep the model list; every provider unless `-p a,b` narrows it |
|
|
309
341
|
| `models --provider <id> --metadata` | inspect field sources, conflicts and reference pricing as JSON |
|
|
310
342
|
| `prune <id>` / `remove <id> [--prune]` | remove from app configs / from the store |
|
|
311
343
|
| `remove <id> --apps omp` | delete only the selected app's entry; supports `--dry-run` |
|
package/README.zh-CN.md
CHANGED
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
|
|
26
26
|
- **它能去桌面应用去不了的地方。** 无头服务器、容器、devcontainer、CI。
|
|
27
27
|
`npx agentsw use myproxy -a codex,omp` 在开机脚本里是一行;GUI 点九次不是。
|
|
28
|
-
- **`--dry-run` 输出脱敏 diff。**
|
|
29
|
-
|
|
28
|
+
- **`--dry-run` 输出脱敏 diff。** 写盘前先预览;预览不写文件、不创建备份,供应商 store 也一样——
|
|
29
|
+
预览 `sync` 不会把刷新到的模型列表留在配置里。正式提交变更时,已有配置文件才会先备份。
|
|
30
30
|
- **默认不破坏现有配置。** 只覆盖 agentsw 自己管理的字段:它不建模的供应商级键
|
|
31
31
|
(`authHeader`、`headers`、`compat`、`discovery`)、`thinkingLevelMap` 之类的模型级字段、
|
|
32
32
|
以及 YAML 注释,都会在重新同步后原样保留。
|
|
@@ -87,7 +87,7 @@ agentsw # 全局安装后可用;短名 `asw` 完全等价
|
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
**不带参数运行就是一个菜单——整个工具都在里面。** 添加供应商(自动识别或手动设置)、导入各智能体
|
|
90
|
-
|
|
90
|
+
已有的供应商、切换供应商和默认模型、同步供应商(重新获取模型列表再写入)、设置模型参数补充源、重命名或删除、
|
|
91
91
|
安装/升级智能体、切换语言。首次运行会先问你要 English 还是简体中文,并提议接管你各智能体已经在用的配置。
|
|
92
92
|
|
|
93
93
|
菜单里的每项操作都有对应的命令,方便脚本、CI 和无头机器——见 [命令](#命令)。
|
|
@@ -200,6 +200,10 @@ asw add -y --discover --exclude "*embedding*,*video*" --id myproxy ...
|
|
|
200
200
|
asw add -y --discover --no-dedup ... # 保留重复项
|
|
201
201
|
asw discover myproxy --include "gpt-*,glm-*" # 更新持久化过滤规则
|
|
202
202
|
asw discover myproxy --no-filter # 清除规则
|
|
203
|
+
|
|
204
|
+
# 只刷新模型列表、不写任何智能体配置,可一次多个。
|
|
205
|
+
asw discover --all
|
|
206
|
+
asw discover myproxy backup-proxy
|
|
203
207
|
```
|
|
204
208
|
|
|
205
209
|
手动 `--models` 指定的 id 和默认模型永不被过滤。
|
|
@@ -234,9 +238,16 @@ asw refresh --provider myproxy --metadata-mode off
|
|
|
234
238
|
# 将显式关闭的供应商恢复为自动模式
|
|
235
239
|
asw refresh --provider myproxy --metadata-mode auto
|
|
236
240
|
|
|
237
|
-
#
|
|
241
|
+
# 单独预览、应用。sync 会先重新获取模型列表;
|
|
242
|
+
# --dry-run 同样会获取,预览因此与真实 sync 一致,但什么都不保存;
|
|
243
|
+
# --no-refresh 则完全不联网,只写入已保存的列表。
|
|
238
244
|
asw sync --provider myproxy --dry-run
|
|
239
245
|
asw sync --provider myproxy
|
|
246
|
+
asw sync --provider myproxy --no-refresh
|
|
247
|
+
|
|
248
|
+
# 一次刷新并推送多个供应商。
|
|
249
|
+
asw sync --all
|
|
250
|
+
asw sync --provider myproxy,backup-proxy
|
|
240
251
|
```
|
|
241
252
|
|
|
242
253
|
交互菜单的“设置并刷新模型参数补充源”提供“自动按需(推荐)”“始终补充”“关闭”,
|
|
@@ -266,8 +277,25 @@ asw sync --provider myproxy
|
|
|
266
277
|
审计信息仅保存在 agentsw,不写入智能体运行配置。Gateway 目录独立缓存 24 小时;请求失败会先重试一次,
|
|
267
278
|
仍失败才使用旧缓存,无缓存时只跳过补充。`AGENTSW_DEBUG=1` 会说明该次尝试失败的原因,
|
|
268
279
|
`AGENTSW_GATEWAY_TIMEOUT_MS` 可覆盖默认 15 秒的整体请求预算。
|
|
269
|
-
-
|
|
270
|
-
|
|
280
|
+
- **端点下掉的模型会被明确报出,而不是悄悄保留。** 刷新时即使列表里不再有默认模型和小模型,也会保留它们——
|
|
281
|
+
一次抖动的列表不该删掉你选定的模型——但「removed upstream」里就不会出现它们了,所以改为在写入之后用
|
|
282
|
+
`warning:` 点名,`--json` 里对应 `refreshed.missing`。模型被改名时从客户端看到的就是这个现象:
|
|
283
|
+
配置照旧,而端点已经不认这个 id 了。
|
|
284
|
+
- **选择供应商的方式统一:** `sync`、`discover`、`refresh` 都支持 `--all` 和逗号分隔的 `-p a,b`,
|
|
285
|
+
`discover` 还可以直接写位置参数(`discover alpha beta`)。所有 id 在第一次请求前就全部解析,
|
|
286
|
+
写错一个会在任何东西落盘之前失败。不指定时各命令保持自己的默认:`sync` 和 `discover` 取当前供应商,
|
|
287
|
+
`refresh` 取全部。
|
|
288
|
+
- **一次同步多个供应商:** `sync --all` 取全部已配置的供应商,`-p a,b` 取指定列表;逐个刷新并写入,
|
|
289
|
+
并把当前供应商放在最后,这样各智能体记录的“当前指向”最终就停在 store 所说的那一个。
|
|
290
|
+
只记录单个供应商(而非供应商表)的应用——例如 Claude Code 的 env 块——只写一次而不是每个供应商写一遍:
|
|
291
|
+
能接受当前供应商时就写它,不能接受时(Claude Code 只收 anthropic 端点,当前供应商却可能是 openai 的)
|
|
292
|
+
就写本次同步里它唯一能接受的那个。若有多个候选且当前供应商不在其中,选择权在你,它保持原有指向:
|
|
293
|
+
同步不是切换。
|
|
294
|
+
某个供应商的端点不可达时会被报告并跳过,其余照常同步,整个运行以非零状态结束。
|
|
295
|
+
- **自动查询不等于自动推送:** 添加、快速添加、发现、导入、刷新时补全保存的元数据,永远不会自行写入智能体配置。
|
|
296
|
+
推送是 `sync`,而且它先刷新:先获取供应商模型列表、按目录补全、保存到 store,再写入各智能体。
|
|
297
|
+
`sync --dry-run` 会获取同一份列表以保证预览准确,但既不保存列表也不写配置;
|
|
298
|
+
`sync --no-refresh` 不联网,只写入已保存的列表;`discover` 刷新完就停,不写任何智能体。
|
|
271
299
|
|
|
272
300
|
## 命令
|
|
273
301
|
|
|
@@ -280,11 +308,11 @@ asw sync --provider myproxy
|
|
|
280
308
|
| `list` / `status` | 已配置的供应商 / 各智能体当前指向 |
|
|
281
309
|
| `list --apps omp,prime` | 列出智能体本地供应商 ID,包括未导入 agentsw 的条目 |
|
|
282
310
|
| `rename <id> <new-id>` | 备份并迁移 ID 和配置引用,支持 `--dry-run` |
|
|
283
|
-
| `use <id>` | 切换所有检测到的智能体;`-a codex,omp`、`-m <model>`、`--dry-run` |
|
|
284
|
-
| `sync` |
|
|
285
|
-
| `discover
|
|
311
|
+
| `use <id>` | 切换所有检测到的智能体;`-a codex,omp`、`-m <model>`、`--small-model <id>` / `--no-small-model`、`--dry-run` |
|
|
312
|
+
| `sync` | 先刷新各供应商的模型列表,再写入各智能体配置;`--all` 或 `-p a,b` 可一次处理多个,`--no-refresh` 只写已保存的列表 |
|
|
313
|
+
| `discover [ids...] [--sync]` | 从 `/v1/models` 刷新模型列表与元数据;`--all` 或多个 id 可一次处理多个,不给 id 则取当前供应商;`--sync` 再推送到各智能体 |
|
|
286
314
|
| `models [query]` | 搜索 models.dev 目录(`-r` 强制刷新缓存,`-l <n>` 限制条数) |
|
|
287
|
-
| `refresh [--provider <
|
|
315
|
+
| `refresh [--provider <ids>]` | 刷新已有模型参数,可设置 Gateway 补充源,不改变模型列表;默认全部,`-p a,b` 可缩小范围 |
|
|
288
316
|
| `models --provider <id> --metadata` | 以 JSON 查看字段来源、冲突和参考价格 |
|
|
289
317
|
| `prune <id>` / `remove <id> [--prune]` | 从各应用配置中清除 / 从存储中删除 |
|
|
290
318
|
| `remove <id> --apps omp` | 仅删除指定智能体内的条目,支持 `--dry-run` |
|
package/dist/commands.js
CHANGED
|
@@ -17,7 +17,7 @@ import { SafeConfigError, drainPendingWrites, readTextIfExists, setDryRun } from
|
|
|
17
17
|
import { applyModelFilter } from "./filter.js";
|
|
18
18
|
import { availableProviderId, providerIdFromBaseUrl, providerNameFromBaseUrl } from "./slug.js";
|
|
19
19
|
import { t } from "./i18n.js";
|
|
20
|
-
import { cancelInteractive, emitJson, error, isJson, note, out, table } from "./ui.js";
|
|
20
|
+
import { cancelInteractive, emitJson, error, isJson, note, out, table, warn } from "./ui.js";
|
|
21
21
|
import { withProgress } from "./progress.js";
|
|
22
22
|
import { appsReport, listReport, modelsQueryReport, providerMetadataReport, providerModelsReport, statusReport, } from "./report.js";
|
|
23
23
|
/** Share successes and failures without fetching until a model actually needs the supplement. */
|
|
@@ -491,7 +491,7 @@ function reportResults(results) {
|
|
|
491
491
|
out(` ${" ".repeat(9)} ${pc.dim(note)}`);
|
|
492
492
|
}
|
|
493
493
|
}
|
|
494
|
-
async function runTargets(op, provider, appsFilter,
|
|
494
|
+
async function runTargets(op, provider, appsFilter, opts = {}) {
|
|
495
495
|
const selected = resolveTargets(appsFilter);
|
|
496
496
|
const explicit = appsFilter !== undefined && appsFilter !== "all";
|
|
497
497
|
const results = [];
|
|
@@ -505,6 +505,18 @@ async function runTargets(op, provider, appsFilter, redactErrors = false) {
|
|
|
505
505
|
});
|
|
506
506
|
continue;
|
|
507
507
|
}
|
|
508
|
+
if (op === "apply" && opts.oneSlot && target.singleProvider && provider.id !== opts.oneSlot.winners.get(target.id)) {
|
|
509
|
+
const winner = opts.oneSlot.winners.get(target.id);
|
|
510
|
+
results.push({
|
|
511
|
+
app: target.id,
|
|
512
|
+
changed: [],
|
|
513
|
+
notes: [],
|
|
514
|
+
skipped: winner === undefined
|
|
515
|
+
? `${target.name} holds one provider at a time; left as it is because this run does not say which`
|
|
516
|
+
: `${target.name} holds one provider at a time; written for ${winner}`,
|
|
517
|
+
});
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
508
520
|
try {
|
|
509
521
|
if (!explicit && !target.detect()) {
|
|
510
522
|
results.push({ app: target.id, changed: [], notes: [], skipped: `${target.name} not detected (pass --apps ${target.id} to force)` });
|
|
@@ -513,7 +525,7 @@ async function runTargets(op, provider, appsFilter, redactErrors = false) {
|
|
|
513
525
|
results.push(await target[op](provider));
|
|
514
526
|
}
|
|
515
527
|
catch (err) {
|
|
516
|
-
const safe = redactErrors && !(err instanceof SafeConfigError);
|
|
528
|
+
const safe = opts.redactErrors === true && !(err instanceof SafeConfigError);
|
|
517
529
|
results.push({ app: target.id, changed: [], notes: [], skipped: pc.red(`failed: ${safe ? t("preview.failedSafely") : err.message}`) });
|
|
518
530
|
process.exitCode = 1;
|
|
519
531
|
}
|
|
@@ -632,20 +644,20 @@ function printFileDiff(file, next, apiKey) {
|
|
|
632
644
|
if (oldText === newText)
|
|
633
645
|
out(pc.dim(t("preview.onlyRedacted")));
|
|
634
646
|
}
|
|
635
|
-
async function runWithOptionalDryRun(op, provider, apps, dryRun) {
|
|
647
|
+
async function runWithOptionalDryRun(op, provider, apps, dryRun, oneSlot) {
|
|
636
648
|
const summarize = (results, files) => ({
|
|
637
649
|
apps: results.map((r) => r.app),
|
|
638
650
|
files,
|
|
639
651
|
skipped: results.filter((r) => r.skipped).map((r) => ({ app: r.app, reason: r.skipped })),
|
|
640
652
|
});
|
|
641
653
|
if (!dryRun) {
|
|
642
|
-
const results = await runTargets(op, provider, apps);
|
|
654
|
+
const results = await runTargets(op, provider, apps, { oneSlot });
|
|
643
655
|
reportResults(results);
|
|
644
656
|
return summarize(results, results.flatMap((r) => (r.skipped ? [] : r.changed)));
|
|
645
657
|
}
|
|
646
658
|
setDryRun(true);
|
|
647
659
|
try {
|
|
648
|
-
const results = await runTargets(op, provider, apps, true);
|
|
660
|
+
const results = await runTargets(op, provider, apps, { redactErrors: true, oneSlot });
|
|
649
661
|
const writes = drainPendingWrites();
|
|
650
662
|
for (const r of results) {
|
|
651
663
|
if (r.skipped)
|
|
@@ -665,12 +677,25 @@ export async function cmdUse(id, opts) {
|
|
|
665
677
|
resolveTargets(opts.apps);
|
|
666
678
|
const store = loadStore();
|
|
667
679
|
const provider = getProvider(store, id);
|
|
668
|
-
|
|
669
|
-
if (!provider.models.some((m) => m.id ===
|
|
670
|
-
fail(t("use.modelMissing", { model
|
|
680
|
+
const requireConfigured = (model) => {
|
|
681
|
+
if (!provider.models.some((m) => m.id === model)) {
|
|
682
|
+
fail(t("use.modelMissing", { model, id, have: provider.models.map((m) => m.id).join(", ") }));
|
|
671
683
|
}
|
|
684
|
+
};
|
|
685
|
+
if (opts.model) {
|
|
686
|
+
requireConfigured(opts.model);
|
|
672
687
|
provider.defaultModel = opts.model;
|
|
673
688
|
}
|
|
689
|
+
// The slot agents reach for cheap background work — Claude Code's haiku variables. Leaving it
|
|
690
|
+
// unset means "same as the default model", so an expensive default silently becomes the
|
|
691
|
+
// background model too; `--no-small-model` returns the slot to that fallback.
|
|
692
|
+
if (typeof opts.smallModel === "string") {
|
|
693
|
+
requireConfigured(opts.smallModel);
|
|
694
|
+
provider.smallModel = opts.smallModel;
|
|
695
|
+
}
|
|
696
|
+
else if (opts.smallModel === false) {
|
|
697
|
+
delete provider.smallModel;
|
|
698
|
+
}
|
|
674
699
|
if (!opts.dryRun) {
|
|
675
700
|
store.active = id;
|
|
676
701
|
saveStore(store);
|
|
@@ -681,18 +706,189 @@ export async function cmdUse(id, opts) {
|
|
|
681
706
|
emitJson({ provider: id, model: provider.defaultModel, dryRun: opts.dryRun === true, ...outcome });
|
|
682
707
|
}
|
|
683
708
|
}
|
|
709
|
+
/**
|
|
710
|
+
* Shared by `discover` and `sync`: fetch the live model list, re-enrich from the catalog, persist.
|
|
711
|
+
* `pinned` ids survive a listing that dropped them, so they are not reported as "removed upstream".
|
|
712
|
+
* `save: false` keeps the refreshed list in memory for the caller to preview: a dry run shows what a
|
|
713
|
+
* real sync would write without leaving the fetched list behind in the store.
|
|
714
|
+
*/
|
|
715
|
+
async function refreshProviderModels(store, provider, save = true) {
|
|
716
|
+
const listed = await withProgress(t("add.discovering", { url: provider.baseUrl }), (progress) => discoverProviderModels(provider, {
|
|
717
|
+
onPage: ({ page, count }) => progress.update(t("progress.discoverPage", { url: provider.baseUrl, page, count })),
|
|
718
|
+
}));
|
|
719
|
+
const pinned = [provider.defaultModel, provider.smallModel].filter((model) => !!model);
|
|
720
|
+
const outcome = applyModelFilter(listed, provider.modelFilter, pinned);
|
|
721
|
+
reportDropped(outcome.dropped);
|
|
722
|
+
const ids = outcome.kept;
|
|
723
|
+
const retained = new Set([...ids, ...pinned]);
|
|
724
|
+
const known = provider.models.map((m) => m.id);
|
|
725
|
+
const added = ids.filter((m) => !known.includes(m));
|
|
726
|
+
const removed = known.filter((m) => !retained.has(m));
|
|
727
|
+
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog());
|
|
728
|
+
provider.models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, [...retained], { ...provider, modelsDevId: provider.modelsDevId ?? guessProviderHint(catalog, provider.baseUrl) }));
|
|
729
|
+
// Reported by the caller, where it reads after the table or the per-app results instead of
|
|
730
|
+
// scrolling away above them.
|
|
731
|
+
const missing = [...new Set(pinned)].filter((model) => !ids.includes(model));
|
|
732
|
+
if (save)
|
|
733
|
+
saveStore(store);
|
|
734
|
+
return { ids, added, removed, missing, catalog };
|
|
735
|
+
}
|
|
736
|
+
function printModelRefresh(provider, refresh) {
|
|
737
|
+
out(`${pc.bold(provider.id)}: ${refresh.ids.length} models (${pc.green(`+${refresh.added.length}`)} / ${pc.red(`-${refresh.removed.length}`)})` +
|
|
738
|
+
(refresh.added.length ? `\n new: ${refresh.added.join(", ")}` : "") +
|
|
739
|
+
(refresh.removed.length ? `\n removed upstream: ${refresh.removed.join(", ")}` : ""));
|
|
740
|
+
out(table(modelRows(provider.models), MODEL_HEADER, MODEL_TABLE_OPTIONS));
|
|
741
|
+
printUncatalogedHint(provider.models, refresh.catalog, provider.modelsDevId);
|
|
742
|
+
}
|
|
743
|
+
/** Comma-separated ids, the same shape `--apps` takes. Repeats collapse; blanks are ignored. */
|
|
744
|
+
function splitProviderList(value) {
|
|
745
|
+
return [...new Set(value.split(",").map((id) => id.trim()).filter(Boolean))];
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* One provider selection for every command that acts on providers: `--all` takes every configured
|
|
749
|
+
* one, `--provider` takes a comma-separated list, and `fallback` says what no selection means —
|
|
750
|
+
* `"active"` for the commands that write (sync), `"all"` for the ones that only read and refresh.
|
|
751
|
+
* Every id is resolved up front so an unknown one fails before the first network call, never after
|
|
752
|
+
* half a run has been saved. An empty store is an error only when `--all` asked for it by name; the
|
|
753
|
+
* implicit all-providers fallback keeps reporting zero, as it did before there was a flag.
|
|
754
|
+
*/
|
|
755
|
+
function resolveProviderIds(store, opts, fallback) {
|
|
756
|
+
if (opts.all && opts.provider !== undefined)
|
|
757
|
+
fail(t("select.allAndProvider"));
|
|
758
|
+
let ids;
|
|
759
|
+
if (opts.all) {
|
|
760
|
+
ids = Object.keys(store.providers);
|
|
761
|
+
if (!ids.length)
|
|
762
|
+
fail(t("select.noProviders"));
|
|
763
|
+
}
|
|
764
|
+
else if (opts.provider !== undefined) {
|
|
765
|
+
ids = splitProviderList(opts.provider);
|
|
766
|
+
if (!ids.length)
|
|
767
|
+
fail(t("select.noneSelected"));
|
|
768
|
+
}
|
|
769
|
+
else if (fallback === "all") {
|
|
770
|
+
ids = Object.keys(store.providers);
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
if (!store.active)
|
|
774
|
+
fail(t("sync.noActive"));
|
|
775
|
+
ids = [store.active];
|
|
776
|
+
}
|
|
777
|
+
for (const id of ids)
|
|
778
|
+
getProvider(store, id);
|
|
779
|
+
return ids;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Apps that name a single provider are written once (see `RunTargetsOptions.oneSlot`); for the rest
|
|
783
|
+
* the last writer owns whatever "current" pointer the app keeps, so a multi-provider sync finishes
|
|
784
|
+
* on the active provider and leaves the agents where the store says they already are.
|
|
785
|
+
*/
|
|
786
|
+
function activeLast(store, ids) {
|
|
787
|
+
const active = store.active;
|
|
788
|
+
if (ids.length < 2 || !active || !ids.includes(active))
|
|
789
|
+
return ids;
|
|
790
|
+
return [...ids.filter((id) => id !== active), active];
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Which provider each single-slot app should receive. The active provider wins where the app can
|
|
794
|
+
* accept it; where it cannot — Claude Code takes anthropic endpoints only, and the active provider
|
|
795
|
+
* may well be an openai one — the single compatible provider in the run wins instead, because
|
|
796
|
+
* writing the only candidate takes nothing away. With several compatible candidates and no active
|
|
797
|
+
* provider among them the choice is the user's, so the app is left pointing where it already does.
|
|
798
|
+
*/
|
|
799
|
+
function oneSlotWinners(selected, store, ids) {
|
|
800
|
+
const winners = new Map();
|
|
801
|
+
for (const target of selected) {
|
|
802
|
+
if (!target.singleProvider)
|
|
803
|
+
continue;
|
|
804
|
+
const usable = ids.filter((id) => supportsProtocol(target, store.providers[id].protocol));
|
|
805
|
+
const winner = store.active !== undefined && usable.includes(store.active)
|
|
806
|
+
? store.active
|
|
807
|
+
: usable.length === 1 ? usable[0] : undefined;
|
|
808
|
+
if (winner !== undefined)
|
|
809
|
+
winners.set(target.id, winner);
|
|
810
|
+
}
|
|
811
|
+
return winners;
|
|
812
|
+
}
|
|
813
|
+
/** Section separator plus the tail line that says how many of several providers came through. */
|
|
814
|
+
function printMultiSummary(count, failed, key) {
|
|
815
|
+
const done = count - failed;
|
|
816
|
+
out(`\n${failed
|
|
817
|
+
? pc.yellow(t(key === "sync" ? "sync.summaryFailed" : "discover.summaryFailed", { ok: done, count }))
|
|
818
|
+
: t(key === "sync" ? "sync.summaryOk" : "discover.summaryOk", { count })}`);
|
|
819
|
+
}
|
|
684
820
|
export async function cmdSync(opts) {
|
|
685
|
-
resolveTargets(opts.apps);
|
|
821
|
+
const selectedTargets = resolveTargets(opts.apps);
|
|
686
822
|
const store = loadStore();
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
823
|
+
const ids = activeLast(store, resolveProviderIds(store, opts, "active"));
|
|
824
|
+
const many = ids.length > 1;
|
|
825
|
+
const oneSlot = many ? { winners: oneSlotWinners(selectedTargets, store, ids) } : undefined;
|
|
826
|
+
const saveRefresh = opts.dryRun !== true;
|
|
827
|
+
const reports = [];
|
|
828
|
+
let failed = 0;
|
|
829
|
+
for (const [index, id] of ids.entries()) {
|
|
830
|
+
const provider = getProvider(store, id);
|
|
831
|
+
if (index)
|
|
832
|
+
out("");
|
|
833
|
+
let refresh;
|
|
834
|
+
if (opts.refresh !== false) {
|
|
835
|
+
try {
|
|
836
|
+
refresh = await refreshProviderModels(store, provider, saveRefresh);
|
|
837
|
+
}
|
|
838
|
+
catch (err) {
|
|
839
|
+
// One unreachable provider must not strand the others: report it, write nothing for it,
|
|
840
|
+
// and fail the run. A single-provider sync has nothing else to do, so it still throws.
|
|
841
|
+
if (!many)
|
|
842
|
+
throw new Error(`${err.message}\n${t("sync.refreshFailed")}`);
|
|
843
|
+
warn(t("sync.refreshSkipped", { id, message: err.message }));
|
|
844
|
+
process.exitCode = 1;
|
|
845
|
+
failed += 1;
|
|
846
|
+
reports.push({ provider: id, refreshFailed: true });
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (!isJson()) {
|
|
850
|
+
printModelRefresh(provider, refresh);
|
|
851
|
+
// --dry-run promises to write nothing, so say the fetched list only lived long enough to preview.
|
|
852
|
+
if (!saveRefresh)
|
|
853
|
+
note(t("sync.refreshNotSaved"));
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
out(`${t("sync.syncing", { id: pc.bold(id), model: provider.defaultModel })}\n`);
|
|
857
|
+
const outcome = await runWithOptionalDryRun("apply", provider, opts.apps, opts.dryRun, oneSlot);
|
|
858
|
+
if (refresh?.missing.length) {
|
|
859
|
+
const models = refresh.missing.join(", ");
|
|
860
|
+
warn(t(opts.dryRun ? "refresh.pinnedMissing" : "sync.pinnedMissingWritten", { models, id }));
|
|
861
|
+
}
|
|
862
|
+
reports.push({
|
|
863
|
+
provider: id,
|
|
864
|
+
model: provider.defaultModel,
|
|
865
|
+
...(refresh
|
|
866
|
+
? {
|
|
867
|
+
refreshed: {
|
|
868
|
+
models: refresh.ids.length,
|
|
869
|
+
added: refresh.added,
|
|
870
|
+
removed: refresh.removed,
|
|
871
|
+
missing: refresh.missing,
|
|
872
|
+
saved: saveRefresh,
|
|
873
|
+
},
|
|
874
|
+
}
|
|
875
|
+
: {}),
|
|
876
|
+
...outcome,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
693
879
|
if (isJson()) {
|
|
694
|
-
|
|
880
|
+
const first = reports[0] ?? {};
|
|
881
|
+
emitJson(many
|
|
882
|
+
? {
|
|
883
|
+
dryRun: opts.dryRun === true,
|
|
884
|
+
providers: reports,
|
|
885
|
+
files: [...new Set(reports.flatMap((report) => report.files ?? []))],
|
|
886
|
+
}
|
|
887
|
+
: { ...first, dryRun: opts.dryRun === true });
|
|
888
|
+
return;
|
|
695
889
|
}
|
|
890
|
+
if (many)
|
|
891
|
+
printMultiSummary(ids.length, failed, "sync");
|
|
696
892
|
}
|
|
697
893
|
export function cmdStatus() {
|
|
698
894
|
const store = loadStore();
|
|
@@ -770,7 +966,8 @@ export async function cmdModels(query, opts) {
|
|
|
770
966
|
}
|
|
771
967
|
export async function cmdRefreshMeta(opts = {}) {
|
|
772
968
|
const store = loadStore();
|
|
773
|
-
|
|
969
|
+
// Omitting the selection still means every provider: this command only reads catalogs.
|
|
970
|
+
const providers = resolveProviderIds(store, opts, "all").map((id) => getProvider(store, id));
|
|
774
971
|
resolveMetadataOptions(opts);
|
|
775
972
|
for (const provider of providers)
|
|
776
973
|
Object.assign(provider, resolveMetadataOptions(opts, provider));
|
|
@@ -797,50 +994,61 @@ export async function cmdRefreshMeta(opts = {}) {
|
|
|
797
994
|
for (const provider of providers)
|
|
798
995
|
printUncatalogedHint(provider.models, catalog, provider.modelsDevId);
|
|
799
996
|
}
|
|
800
|
-
export async function cmdDiscover(
|
|
997
|
+
export async function cmdDiscover(ids, opts) {
|
|
801
998
|
resolveTargets(opts.apps);
|
|
802
999
|
const store = loadStore();
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1000
|
+
// Ids may arrive as positional arguments, as one comma-separated list, or through `-p` for
|
|
1001
|
+
// symmetry with sync; all three say the same thing, so accept them together.
|
|
1002
|
+
const listed = [...(Array.isArray(ids) ? ids : [ids ?? ""]), opts.provider ?? ""].filter(Boolean).join(",");
|
|
1003
|
+
const selected = resolveProviderIds(store, { provider: listed || undefined, all: opts.all }, "active");
|
|
1004
|
+
// Reject bad metadata options once, before the first provider is contacted.
|
|
1005
|
+
resolveMetadataOptions(opts);
|
|
1006
|
+
const many = selected.length > 1;
|
|
1007
|
+
const reports = [];
|
|
1008
|
+
const refreshed = [];
|
|
1009
|
+
for (const [index, id] of selected.entries()) {
|
|
1010
|
+
const provider = getProvider(store, id);
|
|
1011
|
+
if (index)
|
|
1012
|
+
out("");
|
|
1013
|
+
Object.assign(provider, resolveMetadataOptions(opts, provider));
|
|
1014
|
+
// flags override and re-persist the filter; --no-filter clears it
|
|
1015
|
+
const flagFilter = parseFilterOpts(opts, provider.modelFilter);
|
|
1016
|
+
if (opts.filter === false)
|
|
1017
|
+
provider.modelFilter = undefined;
|
|
1018
|
+
else if (flagFilter)
|
|
1019
|
+
provider.modelFilter = flagFilter;
|
|
1020
|
+
let refresh;
|
|
1021
|
+
try {
|
|
1022
|
+
refresh = await refreshProviderModels(store, provider);
|
|
1023
|
+
}
|
|
1024
|
+
catch (err) {
|
|
1025
|
+
// One unreachable endpoint must not strand the others; a single provider still throws.
|
|
1026
|
+
if (!many)
|
|
1027
|
+
throw err;
|
|
1028
|
+
warn(t("sync.refreshSkipped", { id, message: err.message }));
|
|
1029
|
+
process.exitCode = 1;
|
|
1030
|
+
reports.push({ provider: id, refreshFailed: true });
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
refreshed.push(id);
|
|
1034
|
+
reports.push({ provider: id, models: refresh.ids.length, added: refresh.added, removed: refresh.removed, missing: refresh.missing });
|
|
1035
|
+
if (!isJson())
|
|
1036
|
+
printModelRefresh(provider, refresh);
|
|
1037
|
+
if (refresh.missing.length)
|
|
1038
|
+
warn(t("refresh.pinnedMissing", { models: refresh.missing.join(", "), id }));
|
|
828
1039
|
}
|
|
829
|
-
saveStore(store);
|
|
830
1040
|
if (isJson()) {
|
|
831
|
-
emitJson(
|
|
1041
|
+
emitJson(many ? { providers: reports } : (reports[0] ?? {}));
|
|
832
1042
|
return;
|
|
833
1043
|
}
|
|
834
|
-
|
|
835
|
-
(
|
|
836
|
-
|
|
837
|
-
out(table(modelRows(provider.models), MODEL_HEADER, MODEL_TABLE_OPTIONS));
|
|
838
|
-
printUncatalogedHint(provider.models, catalog, provider.modelsDevId);
|
|
839
|
-
if (opts.sync) {
|
|
1044
|
+
if (many)
|
|
1045
|
+
printMultiSummary(selected.length, selected.length - refreshed.length, "discover");
|
|
1046
|
+
if (opts.sync && refreshed.length) {
|
|
840
1047
|
out("");
|
|
841
|
-
|
|
1048
|
+
// The lists are already fresh: write them without a second fetch.
|
|
1049
|
+
await cmdSync({ provider: refreshed.join(","), apps: opts.apps, refresh: false });
|
|
842
1050
|
}
|
|
843
|
-
else {
|
|
1051
|
+
else if (!opts.sync) {
|
|
844
1052
|
note(`\n${t("discover.next")}`);
|
|
845
1053
|
}
|
|
846
1054
|
}
|
package/dist/i18n.js
CHANGED
|
@@ -14,10 +14,10 @@ const messages = {
|
|
|
14
14
|
"cmd.prune": { en: "remove a provider's entries from app configs (keeps it in agentsw)", "zh-CN": "从应用配置移除供应商条目(仍保留在 agentsw)" },
|
|
15
15
|
"cmd.rename": { en: "rename a provider and migrate config references with backups", "zh-CN": "重命名供应商并迁移配置引用(自动备份)" },
|
|
16
16
|
"cmd.use": { en: "set the active provider and write it into app configs", "zh-CN": "设为当前供应商并写入应用配置" },
|
|
17
|
-
"cmd.sync": { en: "re-apply
|
|
17
|
+
"cmd.sync": { en: "refresh a provider's model list and re-apply it to app configs (--no-refresh writes the saved list)", "zh-CN": "刷新供应商模型列表并重新应用到应用配置(--no-refresh 只写入已保存的列表)" },
|
|
18
18
|
"cmd.status": { en: "show detected apps and what they currently point at", "zh-CN": "显示检测到的应用及当前供应商" },
|
|
19
19
|
"cmd.models": { en: "search models.dev, or show a configured provider's models", "zh-CN": "搜索 models.dev 或显示已配置供应商的模型" },
|
|
20
|
-
"cmd.discover": { en: "refresh
|
|
20
|
+
"cmd.discover": { en: "refresh model lists and configured metadata sources for one or more providers", "zh-CN": "刷新一个或多个供应商的模型列表,并通过已启用的目录补全参数" },
|
|
21
21
|
"cmd.import": { en: "scan app configs, preview and dedupe existing custom providers, then import", "zh-CN": "扫描应用配置,预览并去重已有自定义供应商后导入" },
|
|
22
22
|
"cmd.apps": { en: "check installed agents: current version vs latest", "zh-CN": "检查智能体已安装版本与最新版本" },
|
|
23
23
|
"cmd.install": { en: "install an agent CLI", "zh-CN": "安装智能体 CLI" },
|
|
@@ -35,6 +35,7 @@ const messages = {
|
|
|
35
35
|
"opt.models": { en: "comma-separated model ids", "zh-CN": "逗号分隔的模型 id" },
|
|
36
36
|
"opt.defaultModel": { en: "default model (defaults to first)", "zh-CN": "默认模型(默认取第一个)" },
|
|
37
37
|
"opt.smallModel": { en: "small/fast model (Claude Code haiku slot)", "zh-CN": "小型/快速模型(Claude Code haiku 槽位)" },
|
|
38
|
+
"opt.noSmallModel": { en: "clear the small/fast model so the default model fills that slot", "zh-CN": "清除小型/快速模型,该槽位改用默认模型" },
|
|
38
39
|
"opt.reasoning": { en: "preferred reasoning effort (codex): minimal|low|medium|high", "zh-CN": "Codex 首选推理等级: minimal|low|medium|high" },
|
|
39
40
|
"opt.discover": { en: "list model ids from the provider's /v1/models", "zh-CN": "从供应商 /v1/models 获取模型 id" },
|
|
40
41
|
"opt.include": { en: "keep only models matching comma-separated globs", "zh-CN": "仅保留匹配逗号分隔 glob 的模型" },
|
|
@@ -47,7 +48,7 @@ const messages = {
|
|
|
47
48
|
"opt.noGatewayMetadata": { en: "disable Gateway fetching; keep saved parameters (same as --metadata-mode off)", "zh-CN": "关闭 Gateway 获取,保留已保存参数(等同 --metadata-mode off)" },
|
|
48
49
|
"opt.gatewayModels": { en: 'replace exact aliases as JSON: {"local-id":"creator/model"}; {} clears them', "zh-CN": '用 JSON 替换精确映射:{"本地模型ID":"creator/model"};{} 清空' },
|
|
49
50
|
"opt.metadataDetails": { en: "show field sources, conflicts and Gateway reference pricing as JSON (requires --provider)", "zh-CN": "以 JSON 查看字段来源、冲突和 Gateway 参考价格(需 --provider)" },
|
|
50
|
-
"opt.refreshProvider": { en: "refresh/configure only
|
|
51
|
+
"opt.refreshProvider": { en: "refresh/configure only these saved providers, comma-separated (default: all)", "zh-CN": "仅刷新和设置这些供应商,逗号分隔(默认全部)" },
|
|
51
52
|
"opt.yes": { en: "non-interactive; require all flags", "zh-CN": "非交互模式;要求提供全部参数" },
|
|
52
53
|
"opt.prune": { en: "also remove entries from app configs", "zh-CN": "同时从应用配置中移除条目" },
|
|
53
54
|
"opt.apps": { en: "comma-separated apps or 'all'", "zh-CN": "逗号分隔的应用或 'all'" },
|
|
@@ -56,9 +57,12 @@ const messages = {
|
|
|
56
57
|
"opt.appsDetailed": { en: "comma-separated apps (claude,codex,omp,pi,prime,opencode,hermes,workbuddy,dsh) or 'all'", "zh-CN": "逗号分隔的应用(claude,codex,omp,pi,prime,opencode,hermes,workbuddy,dsh)或 'all'" },
|
|
57
58
|
"opt.model": { en: "override default model while switching", "zh-CN": "切换时覆盖默认模型" },
|
|
58
59
|
"opt.dryRun": { en: "preview config diff without writing", "zh-CN": "仅预览配置差异,不写入" },
|
|
59
|
-
"opt.provider": { en: "sync
|
|
60
|
+
"opt.provider": { en: "sync these providers instead of the active one (comma-separated)", "zh-CN": "同步指定供应商而非当前供应商(逗号分隔)" },
|
|
61
|
+
"opt.syncAll": { en: "sync every configured provider", "zh-CN": "同步全部已配置的供应商" },
|
|
62
|
+
"opt.refreshAll": { en: "refresh every configured provider", "zh-CN": "刷新全部已配置的供应商" },
|
|
60
63
|
"opt.showProvider": { en: "show models of a configured provider", "zh-CN": "显示已配置供应商的模型" },
|
|
61
64
|
"opt.refresh": { en: "force-refresh models.dev cache", "zh-CN": "强制刷新 models.dev 缓存" },
|
|
65
|
+
"opt.noRefresh": { en: "skip the model-list refresh; write the saved list only", "zh-CN": "跳过模型列表刷新;只写入已保存的列表" },
|
|
62
66
|
"opt.limit": { en: "maximum results", "zh-CN": "最大结果数" },
|
|
63
67
|
"opt.syncAfter": { en: "push refreshed provider into app configs", "zh-CN": "刷新后推送到应用配置" },
|
|
64
68
|
"opt.appsSync": { en: "apps to sync when --sync is set", "zh-CN": "--sync 时要同步的应用" },
|
|
@@ -80,8 +84,7 @@ const messages = {
|
|
|
80
84
|
"menu.use": { en: "Switch provider and default model", "zh-CN": "切换供应商和默认模型" },
|
|
81
85
|
"menu.status": { en: "View each agent's current configuration", "zh-CN": "查看各智能体当前配置" },
|
|
82
86
|
"menu.list": { en: "View providers saved in agentsw", "zh-CN": "查看 agentsw 供应商列表" },
|
|
83
|
-
"menu.sync": { en: "
|
|
84
|
-
"menu.discover": { en: "Update a provider's model list", "zh-CN": "更新供应商模型列表" },
|
|
87
|
+
"menu.sync": { en: "Sync providers", "zh-CN": "同步供应商" },
|
|
85
88
|
"menu.metadata": { en: "Configure and refresh model metadata", "zh-CN": "设置并刷新模型参数补充源" },
|
|
86
89
|
"menu.metadataHelp": { en: "Keep the model list; choose automatic, always-on or disabled AI Gateway lookup, then refresh saved parameters only.", "zh-CN": "保留模型列表,选择 AI Gateway 自动按需、始终补充或关闭,仅刷新保存的参数。" },
|
|
87
90
|
"menu.metadataProvider": { en: "which provider's metadata settings should be updated?", "zh-CN": "选择要设置模型参数补充源的供应商" },
|
|
@@ -128,8 +131,8 @@ const messages = {
|
|
|
128
131
|
"menu.appsAll": { en: "all {count} detected apps", "zh-CN": "全部 {count} 个检测到的应用" },
|
|
129
132
|
"menu.appNotDetected": { en: "(not detected)", "zh-CN": "(未检测到)" },
|
|
130
133
|
"menu.appsHint": { en: "↑/↓ move · Space toggle · Enter confirm", "zh-CN": "↑/↓ 移动 · 空格切换 · Enter 确认" },
|
|
131
|
-
"menu.
|
|
132
|
-
"menu.
|
|
134
|
+
"menu.syncFor": { en: "which providers should be synced?", "zh-CN": "选择要同步的供应商(可多选)" },
|
|
135
|
+
"menu.providersAll": { en: "all {count} configured providers", "zh-CN": "全部 {count} 个已配置供应商" },
|
|
133
136
|
"menu.removeProvider": { en: "which provider configuration should be deleted?", "zh-CN": "选择要删除配置的供应商" },
|
|
134
137
|
"menu.upgrade": { en: "upgrade all installed agents that have updates?", "zh-CN": "是否升级所有有新版本的已安装智能体?" },
|
|
135
138
|
"menu.installApp": { en: "Install a new coding agent", "zh-CN": "安装新的智能体" },
|
|
@@ -205,8 +208,7 @@ const messages = {
|
|
|
205
208
|
"menu.useHelp": { en: "Select a provider and default model, then write them to compatible agent configs.", "zh-CN": "选择供应商和默认模型,然后写入支持该协议的智能体配置。" },
|
|
206
209
|
"menu.statusHelp": { en: "Show detected agents, their current providers and config file locations.", "zh-CN": "查看检测到的智能体、所用供应商及配置文件位置。" },
|
|
207
210
|
"menu.listHelp": { en: "Show only the providers, protocols and default models saved in agentsw.", "zh-CN": "只查看 agentsw 已保存的供应商、协议和默认模型。" },
|
|
208
|
-
"menu.syncHelp": { en: "
|
|
209
|
-
"menu.discoverHelp": { en: "Fetch models from the provider, fill in model metadata, and optionally sync to agents.", "zh-CN": "从供应商接口重新获取模型并补全模型参数,可选择同步到智能体。" },
|
|
211
|
+
"menu.syncHelp": { en: "Fetch each selected provider's current model list, fill in saved parameters, then write them to the selected agent configs.", "zh-CN": "依次获取所选供应商的最新模型列表并补全保存的参数,然后写入所选智能体配置。" },
|
|
210
212
|
"menu.renameHelp": { en: "Change the config ID and update references; keep custom display names.", "zh-CN": "修改配置中的标识并更新关联引用,保留自定义显示名称。" },
|
|
211
213
|
"menu.removeHelp": { en: "Choose where to delete: agentsw only, one agent only, or both.", "zh-CN": "先选择删除范围:仅 agentsw、仅某个智能体,或两处一起。" },
|
|
212
214
|
"menu.appsHelp": { en: "Compare installed and latest versions, then choose whether to upgrade.", "zh-CN": "对比已安装版本和最新版本,再选择是否升级。" },
|
|
@@ -241,6 +243,14 @@ const messages = {
|
|
|
241
243
|
"use.switching": { en: "switching to {id} ({protocol}) · default model {model}", "zh-CN": "正在切换到 {id} ({protocol})· 默认模型 {model}" },
|
|
242
244
|
"sync.noActive": { en: "no active provider; run agentsw use <id> first", "zh-CN": "没有当前供应商;请先运行 agentsw use <id>" },
|
|
243
245
|
"sync.syncing": { en: "syncing provider {id} · default model {model}", "zh-CN": "正在同步供应商 {id} · 默认模型 {model}" },
|
|
246
|
+
"select.allAndProvider": { en: "--all cannot be combined with a provider selection", "zh-CN": "--all 不能与指定的供应商同时使用" },
|
|
247
|
+
"select.noProviders": { en: "no providers configured; run agentsw add first", "zh-CN": "尚未配置任何供应商;请先运行 agentsw add" },
|
|
248
|
+
"select.noneSelected": { en: "the provider selection is empty; name at least one id", "zh-CN": "未指定任何供应商,至少需要一个 id" },
|
|
249
|
+
"sync.refreshSkipped": { en: "skipping {id}: {message}", "zh-CN": "已跳过 {id}:{message}" },
|
|
250
|
+
"sync.summaryOk": { en: "synced {count} providers", "zh-CN": "已同步 {count} 个供应商" },
|
|
251
|
+
"sync.summaryFailed": { en: "synced {ok} of {count} providers; the rest were skipped", "zh-CN": "已同步 {count} 个中的 {ok} 个供应商,其余已跳过" },
|
|
252
|
+
"sync.refreshNotSaved": { en: "dry run: the refreshed model list was not saved", "zh-CN": "试运行:刷新到的模型列表未保存" },
|
|
253
|
+
"sync.refreshFailed": { en: "could not refresh the model list; pass --no-refresh to write the saved list instead", "zh-CN": "无法刷新模型列表;可以加 --no-refresh 只写入已保存的列表" },
|
|
244
254
|
"status.config": { en: "config: {file}", "zh-CN": "配置:{file}" },
|
|
245
255
|
"status.active": { en: "active provider: {id}", "zh-CN": "当前供应商:{id}" },
|
|
246
256
|
"status.none": { en: "(none)", "zh-CN": "(无)" },
|
|
@@ -375,9 +385,15 @@ const messages = {
|
|
|
375
385
|
},
|
|
376
386
|
"upgrade.done": { en: "{id} -> {version}", "zh-CN": "{id} -> {version}" },
|
|
377
387
|
"upgrade.failed": { en: "{id} upgrade failed: {message}", "zh-CN": "{id} 升级失败:{message}" },
|
|
378
|
-
"discover.
|
|
379
|
-
|
|
380
|
-
|
|
388
|
+
"discover.summaryOk": { en: "refreshed {count} providers", "zh-CN": "已刷新 {count} 个供应商" },
|
|
389
|
+
"discover.summaryFailed": { en: "refreshed {ok} of {count} providers; the rest were skipped", "zh-CN": "已刷新 {count} 个中的 {ok} 个供应商,其余已跳过" },
|
|
390
|
+
"refresh.pinnedMissing": {
|
|
391
|
+
en: "{models} no longer listed by the endpoint; kept as configured — pick a listed model with: agentsw use {id} -m <model>",
|
|
392
|
+
"zh-CN": "{models} 已不在端点的模型列表中;仍按原配置保留 —— 可用 agentsw use {id} -m <model> 换成列表中的模型",
|
|
393
|
+
},
|
|
394
|
+
"sync.pinnedMissingWritten": {
|
|
395
|
+
en: "{models} no longer listed by the endpoint but was written to the agents anyway; pick a listed model with: agentsw use {id} -m <model>",
|
|
396
|
+
"zh-CN": "{models} 已不在端点的模型列表中,但仍写入了各智能体;可用 agentsw use {id} -m <model> 换成列表中的模型",
|
|
381
397
|
},
|
|
382
398
|
"discover.next": {
|
|
383
399
|
en: "run `agentsw sync` to push into app configs",
|
package/dist/index.js
CHANGED
|
@@ -133,14 +133,18 @@ program
|
|
|
133
133
|
.description(t("cmd.use"))
|
|
134
134
|
.option("-a, --apps <apps>", t("opt.appsDetailed"))
|
|
135
135
|
.option("-m, --model <id>", t("opt.model"))
|
|
136
|
+
.option("--small-model <id>", t("opt.smallModel"))
|
|
137
|
+
.option("--no-small-model", t("opt.noSmallModel"))
|
|
136
138
|
.option("-n, --dry-run", t("opt.dryRun"))
|
|
137
139
|
.action(cmdUse);
|
|
138
140
|
program
|
|
139
141
|
.command("sync")
|
|
140
142
|
.description(t("cmd.sync"))
|
|
141
143
|
.option("-a, --apps <apps>", t("opt.apps"))
|
|
142
|
-
.option("-p, --provider <
|
|
144
|
+
.option("-p, --provider <ids>", t("opt.provider"))
|
|
145
|
+
.option("--all", t("opt.syncAll"))
|
|
143
146
|
.option("-n, --dry-run", t("opt.dryRun"))
|
|
147
|
+
.option("--no-refresh", t("opt.noRefresh"))
|
|
144
148
|
.action(cmdSync);
|
|
145
149
|
program.command("status").description(t("cmd.status")).action(cmdStatus);
|
|
146
150
|
program
|
|
@@ -152,8 +156,10 @@ program
|
|
|
152
156
|
.option("-l, --limit <n>", t("opt.limit"), "30")
|
|
153
157
|
.action(cmdModels);
|
|
154
158
|
program
|
|
155
|
-
.command("discover
|
|
159
|
+
.command("discover [ids...]")
|
|
156
160
|
.description(t("cmd.discover"))
|
|
161
|
+
.option("-p, --provider <ids>", t("opt.refreshProvider"))
|
|
162
|
+
.option("--all", t("opt.refreshAll"))
|
|
157
163
|
.option("-s, --sync", t("opt.syncAfter"))
|
|
158
164
|
.option("-a, --apps <apps>", t("opt.appsSync"))
|
|
159
165
|
.option("--include <globs>", t("opt.setInclude"))
|
|
@@ -181,7 +187,8 @@ program.command("upgrade [apps...]").description(t("cmd.upgrade")).action(cmdUpg
|
|
|
181
187
|
program
|
|
182
188
|
.command("refresh")
|
|
183
189
|
.description(t("cmd.refresh"))
|
|
184
|
-
.option("-p, --provider <
|
|
190
|
+
.option("-p, --provider <ids>", t("opt.refreshProvider"))
|
|
191
|
+
.option("--all", t("opt.refreshAll"))
|
|
185
192
|
.option("--metadata-mode <auto|on|off>", t("opt.metadataMode"))
|
|
186
193
|
.option("--gateway-metadata", t("opt.gatewayMetadata"))
|
|
187
194
|
.option("--no-gateway-metadata", t("opt.noGatewayMetadata"))
|
package/dist/menu.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
2
|
import prompts from "prompts";
|
|
3
|
-
import { cmdAdd, cmdQuickAdd, cmdApps, cmdInstall,
|
|
3
|
+
import { cmdAdd, cmdQuickAdd, cmdApps, cmdInstall, cmdImport, cmdRefreshMeta, cmdList, cmdStatus, cmdSync, cmdUpgrade, cmdUse, } from "./commands.js";
|
|
4
4
|
import { getLocale, setLocale, t } from "./i18n.js";
|
|
5
5
|
import { getMetadataMode } from "./metadata.js";
|
|
6
6
|
import { loadStore, saveStore } from "./store.js";
|
|
@@ -66,6 +66,41 @@ async function pickProvider(message) {
|
|
|
66
66
|
const p = store.providers[id];
|
|
67
67
|
return { id, defaultModel: p.defaultModel, models: p.models.map((m) => m.id) };
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Multi-select providers for one sync. The first row takes every configured provider and maps to
|
|
71
|
+
* the CLI's `--all`; toggling everything off means "back", not "all", because a sync writes files.
|
|
72
|
+
*/
|
|
73
|
+
async function pickProviders(message) {
|
|
74
|
+
const store = loadStore();
|
|
75
|
+
const ids = Object.keys(store.providers);
|
|
76
|
+
if (ids.length === 0) {
|
|
77
|
+
out(pc.yellow(t("menu.noProvidersHint")));
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
const { providers } = await prompts({
|
|
81
|
+
type: "multiselect",
|
|
82
|
+
name: "providers",
|
|
83
|
+
message,
|
|
84
|
+
hint: t("menu.appsHint"),
|
|
85
|
+
instructions: false,
|
|
86
|
+
choices: [
|
|
87
|
+
{ title: t("menu.providersAll", { count: ids.length }), value: "__all__" },
|
|
88
|
+
...ids.map((pid) => {
|
|
89
|
+
const p = store.providers[pid];
|
|
90
|
+
return {
|
|
91
|
+
title: `${pid} · ${p.protocol} · ${t("menu.defaultModel")} ${p.defaultModel}${store.active === pid ? ` (${t("menu.active")})` : ""}`,
|
|
92
|
+
value: pid,
|
|
93
|
+
selected: store.active === pid,
|
|
94
|
+
};
|
|
95
|
+
}),
|
|
96
|
+
],
|
|
97
|
+
}, cancel);
|
|
98
|
+
const chosen = Array.isArray(providers) ? providers : [];
|
|
99
|
+
if (chosen.includes("__all__"))
|
|
100
|
+
return { all: true, ids };
|
|
101
|
+
const selected = chosen.filter((value) => value !== "__all__");
|
|
102
|
+
return selected.length ? { all: false, ids: selected } : undefined;
|
|
103
|
+
}
|
|
69
104
|
/**
|
|
70
105
|
* Multi-select target apps, remembering the previous choice. The first row selects
|
|
71
106
|
* every detected app; picking it (or toggling everything off) returns undefined,
|
|
@@ -212,7 +247,6 @@ export async function cmdMenu() {
|
|
|
212
247
|
{ title: t("menu.status"), description: t("menu.statusHelp"), value: "status" },
|
|
213
248
|
{ title: t("menu.list"), description: t("menu.listHelp"), value: "list" },
|
|
214
249
|
{ title: t("menu.sync"), description: t("menu.syncHelp"), value: "sync" },
|
|
215
|
-
{ title: t("menu.discover"), description: t("menu.discoverHelp"), value: "discover" },
|
|
216
250
|
{ title: t("menu.metadata"), description: t("menu.metadataHelp"), value: "metadata" },
|
|
217
251
|
{ title: t("menu.rename"), description: t("menu.renameHelp"), value: "rename" },
|
|
218
252
|
{ title: t("menu.remove"), description: t("menu.removeHelp"), value: "remove" },
|
|
@@ -271,14 +305,11 @@ export async function cmdMenu() {
|
|
|
271
305
|
cmdList();
|
|
272
306
|
}
|
|
273
307
|
else if (action === "sync") {
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
else if (action === "discover") {
|
|
277
|
-
const picked = await pickProvider(t("menu.discoverFor"));
|
|
308
|
+
const picked = await pickProviders(t("menu.syncFor"));
|
|
278
309
|
if (!picked)
|
|
279
310
|
continue;
|
|
280
|
-
const
|
|
281
|
-
await
|
|
311
|
+
const apps = await pickApps(t("menu.pickApps"));
|
|
312
|
+
await cmdSync(picked.all ? { all: true, apps } : { provider: picked.ids.join(","), apps });
|
|
282
313
|
}
|
|
283
314
|
else if (action === "metadata") {
|
|
284
315
|
const picked = await pickProvider(t("menu.metadataProvider"));
|
|
@@ -14,6 +14,8 @@ export const claudecode = transactionalTarget({
|
|
|
14
14
|
id: "claude",
|
|
15
15
|
name: "Claude Code",
|
|
16
16
|
protocols: ["anthropic"],
|
|
17
|
+
// The env block names one endpoint: a second provider replaces the first rather than joining it.
|
|
18
|
+
singleProvider: true,
|
|
17
19
|
get configPaths() {
|
|
18
20
|
return [claudeSettingsFile()];
|
|
19
21
|
},
|
package/dist/targets/dsh.js
CHANGED
|
@@ -5,7 +5,7 @@ import { dshCredentialsFile, dshDir, dshSettingsFile } from "../app-paths.js";
|
|
|
5
5
|
import { isManagedCredentialRef, legacyManagedCredentialRef, managedCredentialRef } from "../provider-identity.js";
|
|
6
6
|
import { transactionalTarget } from "../target-transaction.js";
|
|
7
7
|
import { parseYamlMapping, serializeYamlMapping } from "../yaml.js";
|
|
8
|
-
import { apiValue, classifyApi, entryApi, mergeModels, sdkBaseUrl } from "./wire.js";
|
|
8
|
+
import { apiValue, classifyApi, entryApi, hasModelMetadata, mergeModels, sdkBaseUrl } from "./wire.js";
|
|
9
9
|
/** Layout version of `.credentials.yaml` this build reads and writes. */
|
|
10
10
|
const CREDENTIALS_VERSION = 1;
|
|
11
11
|
/** Thinking levels a `reasoningEfforts` entry may offer (llm-pi-ai THINKING_LEVEL_GATE). */
|
|
@@ -29,7 +29,8 @@ function modelEntry(m) {
|
|
|
29
29
|
...(m.name ? { name: m.name } : {}),
|
|
30
30
|
...(m.contextWindow ? { contextWindow: m.contextWindow } : {}),
|
|
31
31
|
...(m.maxOutput ? { maxTokens: m.maxOutput } : {}),
|
|
32
|
-
input
|
|
32
|
+
// `input` is a guess for a spec that says nothing; see hasModelMetadata.
|
|
33
|
+
...(hasModelMetadata(m) ? { input: m.imageInput ? ["text", "image"] : ["text"] } : {}),
|
|
33
34
|
...(m.reasoning === false
|
|
34
35
|
? { reasoningEfforts: false }
|
|
35
36
|
: m.reasoning && m.reasoningEfforts?.length
|
package/dist/targets/omp.js
CHANGED
|
@@ -5,9 +5,7 @@ import { ompAgentDir, ompModelsFiles } from "../app-paths.js";
|
|
|
5
5
|
import { looksLikeEnvName } from "../slug.js";
|
|
6
6
|
import { transactionalTarget } from "../target-transaction.js";
|
|
7
7
|
import { parseYamlMapping, serializeYamlMapping } from "../yaml.js";
|
|
8
|
-
import { apiValue, classifyApi, entryApi, mergeModels, sdkBaseUrl, stripConflictingOverrides } from "./wire.js";
|
|
9
|
-
/** Per-model keys this adapter writes; one that stops being emitted is cleared, not inherited. */
|
|
10
|
-
const OWNED_MODEL_KEYS = ["id", "name", "reasoning", "input", "contextWindow", "maxTokens", "cost"];
|
|
8
|
+
import { apiValue, applyDeepseekCompat, classifyApi, entryApi, mergeModels, OWNED_MODEL_METADATA_KEYS, ownedModelMetadata, sdkBaseUrl, stripConflictingOverrides, } from "./wire.js";
|
|
11
9
|
/** omp has no directory override; the file set is shared with remove/rename. */
|
|
12
10
|
function modelFiles() {
|
|
13
11
|
const [yml, yaml] = ompModelsFiles();
|
|
@@ -43,27 +41,12 @@ export const omp = transactionalTarget({
|
|
|
43
41
|
const anthropic = provider.protocol === "anthropic";
|
|
44
42
|
const api = apiValue(provider.protocol, provider.openaiApi, prev?.api ?? entryApi(prev ?? {}));
|
|
45
43
|
const baseUrl = sdkBaseUrl(provider.protocol, provider.baseUrl);
|
|
46
|
-
const models = mergeModels(prev?.models, provider.models.map(
|
|
47
|
-
id: m.id,
|
|
48
|
-
...(m.name ? { name: m.name } : {}),
|
|
49
|
-
...(m.reasoning !== undefined ? { reasoning: m.reasoning } : {}),
|
|
50
|
-
input: m.imageInput ? ["text", "image"] : ["text"],
|
|
51
|
-
...(m.contextWindow ? { contextWindow: m.contextWindow } : {}),
|
|
52
|
-
...(m.maxOutput ? { maxTokens: m.maxOutput } : {}),
|
|
53
|
-
...(m.cost
|
|
54
|
-
? {
|
|
55
|
-
cost: {
|
|
56
|
-
input: m.cost.input ?? 0,
|
|
57
|
-
output: m.cost.output ?? 0,
|
|
58
|
-
cacheRead: m.cost.cacheRead ?? 0,
|
|
59
|
-
cacheWrite: m.cost.cacheWrite ?? 0,
|
|
60
|
-
},
|
|
61
|
-
}
|
|
62
|
-
: {}),
|
|
63
|
-
})), OWNED_MODEL_KEYS);
|
|
44
|
+
const models = mergeModels(prev?.models, provider.models.map(ownedModelMetadata), OWNED_MODEL_METADATA_KEYS);
|
|
64
45
|
const conflicts = stripConflictingOverrides(models, api, baseUrl);
|
|
65
46
|
if (conflicts.length)
|
|
66
47
|
notes.push(`dropped model overrides pointing elsewhere: ${conflicts.join(", ")}`);
|
|
48
|
+
if (!anthropic)
|
|
49
|
+
applyDeepseekCompat(models, { requiresReasoningContentForToolCalls: true });
|
|
67
50
|
const entry = {
|
|
68
51
|
baseUrl,
|
|
69
52
|
apiKey: provider.apiKey, // omp treats value as env-var name first, then literal
|
package/dist/targets/pistyle.js
CHANGED
|
@@ -5,18 +5,9 @@ import { piDir, piModelsFile, piSettingsFile } from "../app-paths.js";
|
|
|
5
5
|
import { editJsoncObject, isJsonObject, readJsoncObject } from "../jsonc.js";
|
|
6
6
|
import { looksLikeEnvName } from "../slug.js";
|
|
7
7
|
import { transactionalTarget } from "../target-transaction.js";
|
|
8
|
-
import { apiValue, classifyApi, entryApi, mergeModels, sdkBaseUrl, stripConflictingOverrides } from "./wire.js";
|
|
9
|
-
/**
|
|
10
|
-
const OWNED_MODEL_KEYS = [
|
|
11
|
-
"id",
|
|
12
|
-
"name",
|
|
13
|
-
"reasoning",
|
|
14
|
-
"thinkingLevelMap",
|
|
15
|
-
"input",
|
|
16
|
-
"contextWindow",
|
|
17
|
-
"maxTokens",
|
|
18
|
-
"cost",
|
|
19
|
-
];
|
|
8
|
+
import { apiValue, applyDeepseekCompat, classifyApi, entryApi, mergeModels, OWNED_MODEL_METADATA_KEYS, ownedModelMetadata, sdkBaseUrl, stripConflictingOverrides, } from "./wire.js";
|
|
9
|
+
/** pi writes the shared metadata plus its own thinking-level map. */
|
|
10
|
+
const OWNED_MODEL_KEYS = [...OWNED_MODEL_METADATA_KEYS, "thinkingLevelMap"];
|
|
20
11
|
function readModels(file) {
|
|
21
12
|
const document = readJsoncObject(file);
|
|
22
13
|
const providers = document.value.providers;
|
|
@@ -84,26 +75,17 @@ export function piStyleTarget(opts) {
|
|
|
84
75
|
const api = apiValue(provider.protocol, provider.openaiApi, prev.api ?? entryApi(prev));
|
|
85
76
|
const baseUrl = sdkBaseUrl(provider.protocol, provider.baseUrl);
|
|
86
77
|
const models = mergeModels(prev.models, provider.models.map((m) => ({
|
|
87
|
-
|
|
88
|
-
...(m.name ? { name: m.name } : {}),
|
|
89
|
-
...(m.reasoning !== undefined ? { reasoning: m.reasoning } : {}),
|
|
78
|
+
...ownedModelMetadata(m),
|
|
90
79
|
...(m.reasoning && m.reasoningEfforts?.length
|
|
91
80
|
? { thinkingLevelMap: thinkingLevelMap(m.reasoningEfforts) }
|
|
92
81
|
: {}),
|
|
93
|
-
input: m.imageInput ? ["text", "image"] : ["text"],
|
|
94
|
-
...(m.contextWindow ? { contextWindow: m.contextWindow } : {}),
|
|
95
|
-
...(m.maxOutput ? { maxTokens: m.maxOutput } : {}),
|
|
96
|
-
...(m.cost
|
|
97
|
-
? {
|
|
98
|
-
cost: {
|
|
99
|
-
input: m.cost.input ?? 0,
|
|
100
|
-
output: m.cost.output ?? 0,
|
|
101
|
-
cacheRead: m.cost.cacheRead ?? 0,
|
|
102
|
-
cacheWrite: m.cost.cacheWrite ?? 0,
|
|
103
|
-
},
|
|
104
|
-
}
|
|
105
|
-
: {}),
|
|
106
82
|
})), OWNED_MODEL_KEYS);
|
|
83
|
+
// pi/prime key their DeepSeek wire rules by provider name, so a gateway
|
|
84
|
+
// entry never picks them up; the flags below are what their own deepseek
|
|
85
|
+
// catalog entries carry. Anthropic-wire entries replay differently.
|
|
86
|
+
if (provider.protocol === "openai") {
|
|
87
|
+
applyDeepseekCompat(models, { requiresReasoningContentOnAssistantMessages: true, thinkingFormat: "deepseek" });
|
|
88
|
+
}
|
|
107
89
|
const conflicts = stripConflictingOverrides(models, api, baseUrl);
|
|
108
90
|
if (conflicts.length)
|
|
109
91
|
notes.push(`dropped model overrides pointing elsewhere: ${conflicts.join(", ")}`);
|
package/dist/targets/wire.js
CHANGED
|
@@ -99,6 +99,11 @@ derived = ["input"]) {
|
|
|
99
99
|
const old = prev.get(m.id);
|
|
100
100
|
if (!old)
|
|
101
101
|
return m;
|
|
102
|
+
// An entry that carries nothing but its id says nothing about the model: it
|
|
103
|
+
// is no evidence that the config's metadata went stale, so clearing owned
|
|
104
|
+
// keys on its behalf would delete what a richer sync or the user put there.
|
|
105
|
+
if (Object.keys(m).length === 1)
|
|
106
|
+
return { ...m, ...old };
|
|
102
107
|
const kept = { ...old };
|
|
103
108
|
const known = informative.some((key) => key in m);
|
|
104
109
|
if (known)
|
|
@@ -108,6 +113,66 @@ derived = ["input"]) {
|
|
|
108
113
|
return { ...kept, ...m };
|
|
109
114
|
});
|
|
110
115
|
}
|
|
116
|
+
/** Per-model keys every pi-family adapter owns; see `ownedModelMetadata`. */
|
|
117
|
+
export const OWNED_MODEL_METADATA_KEYS = ["id", "name", "reasoning", "input", "contextWindow", "maxTokens", "cost"];
|
|
118
|
+
const DEEPSEEK_MODEL_ID = /deepseek/i;
|
|
119
|
+
/**
|
|
120
|
+
* omp/pi/prime key their DeepSeek wire rules by provider name ("deepseek",
|
|
121
|
+
* "opencode-go", ...), so a gateway or reseller entry matches none of them: its
|
|
122
|
+
* reasoning replay then breaks and DeepSeek answers 400. Match on the model id
|
|
123
|
+
* instead and write the adapter's own flags per model; an explicit user value wins.
|
|
124
|
+
*/
|
|
125
|
+
export function applyDeepseekCompat(models, flags) {
|
|
126
|
+
for (const model of models) {
|
|
127
|
+
if (!DEEPSEEK_MODEL_ID.test(String(model.id)))
|
|
128
|
+
continue;
|
|
129
|
+
const compat = (model.compat ?? {});
|
|
130
|
+
model.compat = { ...flags, ...compat };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Whether a store entry describes the model at all. A bare id is ignorance,
|
|
135
|
+
* not a catalog statement, so adapters must not clear what the app config
|
|
136
|
+
* already holds on its behalf.
|
|
137
|
+
*/
|
|
138
|
+
export function hasModelMetadata(m) {
|
|
139
|
+
return (m.name !== undefined ||
|
|
140
|
+
m.reasoning !== undefined ||
|
|
141
|
+
m.reasoningEfforts !== undefined ||
|
|
142
|
+
m.imageInput !== undefined ||
|
|
143
|
+
m.contextWindow !== undefined ||
|
|
144
|
+
m.maxInput !== undefined ||
|
|
145
|
+
m.maxOutput !== undefined ||
|
|
146
|
+
m.cost !== undefined);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The per-model metadata agentsw writes into a pi-family models config. omp and
|
|
150
|
+
* the pi/prime adapters share the shape; only the extras around it differ, so
|
|
151
|
+
* keeping one builder is what stops the two from drifting apart.
|
|
152
|
+
*/
|
|
153
|
+
export function ownedModelMetadata(m) {
|
|
154
|
+
// `input` is a guess for a spec that says nothing; writing it would make the
|
|
155
|
+
// entry look authoritative to mergeModels and clear the config's own values.
|
|
156
|
+
const known = hasModelMetadata(m);
|
|
157
|
+
return {
|
|
158
|
+
id: m.id,
|
|
159
|
+
...(m.name ? { name: m.name } : {}),
|
|
160
|
+
...(m.reasoning !== undefined ? { reasoning: m.reasoning } : {}),
|
|
161
|
+
...(known ? { input: m.imageInput ? ["text", "image"] : ["text"] } : {}),
|
|
162
|
+
...(m.contextWindow ? { contextWindow: m.contextWindow } : {}),
|
|
163
|
+
...(m.maxOutput ? { maxTokens: m.maxOutput } : {}),
|
|
164
|
+
...(m.cost
|
|
165
|
+
? {
|
|
166
|
+
cost: {
|
|
167
|
+
input: m.cost.input ?? 0,
|
|
168
|
+
output: m.cost.output ?? 0,
|
|
169
|
+
cacheRead: m.cost.cacheRead ?? 0,
|
|
170
|
+
cacheWrite: m.cost.cacheWrite ?? 0,
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
: {}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
111
176
|
/**
|
|
112
177
|
* Drop per-model `api`/`baseUrl` overrides that contradict the route being
|
|
113
178
|
* written. A model-level override wins over the provider entry in omp and pi,
|
|
@@ -6,6 +6,7 @@ import { workbuddyDir } from "../app-paths.js";
|
|
|
6
6
|
import { localProviderId } from "../provider-identity.js";
|
|
7
7
|
import { providerIdFromBaseUrl, providerNameFromBaseUrl } from "../slug.js";
|
|
8
8
|
import { transactionalTarget } from "../target-transaction.js";
|
|
9
|
+
import { hasModelMetadata } from "./wire.js";
|
|
9
10
|
function configDir() {
|
|
10
11
|
return workbuddyDir();
|
|
11
12
|
}
|
|
@@ -101,12 +102,15 @@ export const workbuddy = transactionalTarget({
|
|
|
101
102
|
const previous = new Map(owned.map((row) => [row.id, row]));
|
|
102
103
|
const ours = provider.models.map((model) => {
|
|
103
104
|
const custom = { ...previous.get(model.id) };
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
// A bare id says nothing about the model; the row keeps what it already has.
|
|
106
|
+
const known = hasModelMetadata(model);
|
|
107
|
+
if (known)
|
|
108
|
+
for (const key of OWNED_MODEL_KEYS)
|
|
109
|
+
delete custom[key];
|
|
106
110
|
return {
|
|
107
111
|
...custom,
|
|
108
112
|
id: model.id,
|
|
109
|
-
name: model.name ?? model.id,
|
|
113
|
+
name: model.name ?? (known ? undefined : custom.name) ?? model.id,
|
|
110
114
|
vendor: provider.name,
|
|
111
115
|
url,
|
|
112
116
|
apiKey: provider.apiKey,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentsw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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": {
|