agentsw 0.11.0 → 0.11.1
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 +12 -6
- package/README.zh-CN.md +5 -2
- package/dist/commands.js +126 -52
- package/dist/discover.js +54 -10
- package/dist/i18n.js +10 -1
- package/dist/index.js +3 -0
- package/dist/metadata.js +35 -5
- package/dist/target-transaction.js +2 -2
- package/dist/targets/pistyle.js +21 -11
- package/dist/ui.js +5 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,9 @@ without touching the settings you hand-tuned.
|
|
|
35
35
|
- **Model metadata, not just an id list.** Reseller `/v1/models` endpoints return bare ids.
|
|
36
36
|
agentsw enriches them from [models.dev](https://models.dev), automatically fills meaningful gaps from AI Gateway when needed, and writes what each app
|
|
37
37
|
actually reads: `thinkingLevelMap` for pi/prime, `limit`/`attachment` for opencode,
|
|
38
|
-
`contextWindow`/`maxTokens` for omp, `reasoningEfforts`/`input` for dsh.
|
|
38
|
+
`contextWindow`/`maxTokens` for omp, `reasoningEfforts`/`input` for dsh. Limits the endpoint declares for
|
|
39
|
+
its own models outrank the catalogs, so one model cannot end up with two different context windows
|
|
40
|
+
depending on which provider lists it.
|
|
39
41
|
- **Adopts what you already have** — including [cc-switch](https://github.com/farion1231/cc-switch).
|
|
40
42
|
`import` scans every installed agent *and* cc-switch's own store, merges duplicates
|
|
41
43
|
(same protocol + normalized endpoint + credentials), and never writes back to either.
|
|
@@ -204,8 +206,9 @@ use `--all` explicitly in scripts. Model discovery may update the local metadata
|
|
|
204
206
|
|
|
205
207
|
## Model discovery filters
|
|
206
208
|
|
|
207
|
-
`--discover` lists model ids from the provider's `/v1/models`, then enriches each id from
|
|
208
|
-
|
|
209
|
+
`--discover` lists model ids from the provider's `/v1/models`, then enriches each id from models.dev first,
|
|
210
|
+
with automatic AI Gateway supplementation when needed. Limits the provider declares for its own models in
|
|
211
|
+
that same listing win over what a catalog claims for them; ids it lists bare are enriched as before. Reseller lists are noisy,
|
|
209
212
|
so filters are persisted per provider and re-applied on every `discover`:
|
|
210
213
|
|
|
211
214
|
```bash
|
|
@@ -277,9 +280,12 @@ Without `--provider`, `refresh` applies to all providers, including any supplied
|
|
|
277
280
|
`--gateway-models` replaces the whole alias map (`{}` clears it); aliases do not change the saved mode or override `off`.
|
|
278
281
|
Import still skips already configured accounts.
|
|
279
282
|
|
|
280
|
-
- **Field-level priority:** manual values > models.dev > Gateway gap filling.
|
|
281
|
-
|
|
282
|
-
|
|
283
|
+
- **Field-level priority:** manual values > the endpoint's own declaration > models.dev > Gateway gap filling.
|
|
284
|
+
An endpoint that advertises `context_window_tokens`/`maximum_output_tokens`
|
|
285
|
+
(or their `context_length`/`max_output_tokens` spellings) for its own list is authoritative for its own
|
|
286
|
+
route, so those limits are written instead of a catalog's estimate for the same model. Tracked automatic
|
|
287
|
+
values may refresh; subsequent manual edits are retained. Legacy values without provenance are conservatively
|
|
288
|
+
treated as manual overrides. Unmodeled extension fields survive enrichment too.
|
|
283
289
|
- **Exact identities:** automatic lookup can map a bare, case-sensitive ID (no `/`) to a unique Gateway
|
|
284
290
|
`creator/model` identity when models.dev evidence is absent or agrees. Ambiguous creator evidence in either
|
|
285
291
|
catalog is rejected; custom or ambiguous names need explicit aliases. Qualified IDs never have their prefixes
|
package/README.zh-CN.md
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
- **写的是元数据,不只是模型 id。** 转售商 `/v1/models` 只返回裸 id;agentsw 从
|
|
34
34
|
[models.dev](https://models.dev) 优先补全,并在有实际缺口时自动查询 AI Gateway,按各应用真正读取的字段下发:pi/prime 的
|
|
35
35
|
`thinkingLevelMap`、opencode 的 `limit`/`attachment`、omp 的 `contextWindow`/`maxTokens`、
|
|
36
|
-
dsh 的 `reasoningEfforts`/`input
|
|
36
|
+
dsh 的 `reasoningEfforts`/`input`。端点自己声明的上下文/输出上限优先于目录,避免同一模型在不同供应商下被写成不同窗口。
|
|
37
37
|
- **接管你已有的配置——包括 [cc-switch](https://github.com/farion1231/cc-switch)。**
|
|
38
38
|
`import` 会扫描所有已安装的智能体**以及 cc-switch 自己的存储**,合并重复项
|
|
39
39
|
(相同协议 + 归一化端点 + 凭据),并且对两者都只读不写。
|
|
@@ -191,6 +191,7 @@ imported zhipu-glm · anthropic · https://open.bigmodel.cn/api/anthropic · 1 m
|
|
|
191
191
|
## 模型发现过滤
|
|
192
192
|
|
|
193
193
|
`--discover` 从供应商 `/v1/models` 拉取模型 id,优先从 models.dev 补全元数据,必要时自动查询 AI Gateway。
|
|
194
|
+
同一份列表里供应商为自家模型声明的上下文/输出上限优先于目录的估算,只给出裸 id 的模型仍按原逻辑补全。
|
|
194
195
|
转售商列表噪音大,过滤规则持久化在供应商上,每次 `discover` 自动复用:
|
|
195
196
|
|
|
196
197
|
```bash
|
|
@@ -260,7 +261,9 @@ asw sync --provider myproxy,backup-proxy
|
|
|
260
261
|
`--gateway-models` 替换整个映射,传 `{}` 清空;映射不会更改保存的模式,也不会覆盖 `off`。
|
|
261
262
|
导入仍跳过已经配置的账号。
|
|
262
263
|
|
|
263
|
-
- **按字段合并:**
|
|
264
|
+
- **按字段合并:** 用户手动值优先,其次端点自己声明的值,然后 models.dev,最后 Gateway 补缺。
|
|
265
|
+
端点在 `/v1/models` 中为自家模型声明了 `context_window_tokens`/`maximum_output_tokens`(或
|
|
266
|
+
`context_length`/`max_output_tokens` 写法)时,以它为准而不是目录对同一模型的估算。已记录来源的自动值可以刷新,
|
|
264
267
|
手动修改后会保留;旧版本中没有来源记录的已有值保守视为手动值,不强制覆盖。未知扩展字段也保留。
|
|
265
268
|
- **精确匹配:** 不含 `/` 的裸 ID 可按大小写精确匹配唯一的 Gateway `creator/model` 身份,
|
|
266
269
|
前提是 models.dev 没有身份线索或线索一致。任一目录中的创建者身份有歧义时拒绝自动匹配;
|
package/dist/commands.js
CHANGED
|
@@ -11,9 +11,9 @@ import { loadCatalog, searchCatalog } from "./modelsdev.js";
|
|
|
11
11
|
import { classifyUnresolved, enrichProviderModels, resolveMetadataOptions } from "./metadata.js";
|
|
12
12
|
import { loadGatewayCatalog } from "./gateway.js";
|
|
13
13
|
import { resolveTargets, supportsProtocol, targets } from "./targets/index.js";
|
|
14
|
-
import {
|
|
14
|
+
import { discoverProviderCatalog, probeProtocols } from "./discover.js";
|
|
15
15
|
import { appCommand, appPackages, installedVersion, isNewer, latestVersion, normalizeAppVersion, runShell } from "./apps.js";
|
|
16
|
-
import { SafeConfigError, drainPendingWrites, readTextIfExists, setDryRun } from "./fsutil.js";
|
|
16
|
+
import { SafeConfigError, drainPendingWrites, isDryRun, readTextIfExists, setDryRun } from "./fsutil.js";
|
|
17
17
|
import { applyModelFilter } from "./filter.js";
|
|
18
18
|
import { availableProviderId, providerIdFromBaseUrl, providerNameFromBaseUrl } from "./slug.js";
|
|
19
19
|
import { t } from "./i18n.js";
|
|
@@ -202,16 +202,18 @@ export async function cmdAdd(opts) {
|
|
|
202
202
|
const metadataOptions = resolveMetadataOptions(opts, existing);
|
|
203
203
|
const manualIds = (answers.models ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
204
204
|
let modelIds = manualIds;
|
|
205
|
+
// Limits the endpoint declares for its own models, kept from the discovery pass.
|
|
206
|
+
let endpointLimits = new Map();
|
|
205
207
|
const modelFilter = parseFilterOpts(opts, existing?.modelFilter);
|
|
206
208
|
const shouldDiscover = opts.discover || manualIds.length === 0;
|
|
207
209
|
if (shouldDiscover) {
|
|
208
210
|
if (!opts.discover) {
|
|
209
211
|
note(t("add.autoDiscover"));
|
|
210
212
|
}
|
|
211
|
-
const discover = () => withProgress(t("add.discovering", { url: baseUrl }), (progress) =>
|
|
213
|
+
const discover = () => withProgress(t("add.discovering", { url: baseUrl }), (progress) => discoverProviderCatalog({ baseUrl, apiKey: answers.apiKey, protocol }, {
|
|
212
214
|
onPage: ({ page, count }) => progress.update(t("progress.discoverPage", { url: baseUrl, page, count })),
|
|
213
215
|
}));
|
|
214
|
-
let discovered = [];
|
|
216
|
+
let discovered = { ids: [], limits: new Map() };
|
|
215
217
|
for (let attempt = 0;; attempt++) {
|
|
216
218
|
try {
|
|
217
219
|
discovered = await discover();
|
|
@@ -228,9 +230,10 @@ export async function cmdAdd(opts) {
|
|
|
228
230
|
break;
|
|
229
231
|
}
|
|
230
232
|
}
|
|
231
|
-
if (discovered.length)
|
|
232
|
-
out(t("add.providerLists", { count: discovered.length }));
|
|
233
|
-
modelIds = [...new Set([...manualIds, ...discovered])];
|
|
233
|
+
if (discovered.ids.length)
|
|
234
|
+
out(t("add.providerLists", { count: discovered.ids.length }));
|
|
235
|
+
modelIds = [...new Set([...manualIds, ...discovered.ids])];
|
|
236
|
+
endpointLimits = discovered.limits;
|
|
234
237
|
}
|
|
235
238
|
const retained = [opts.defaultModel === undefined ? existing?.defaultModel : undefined, opts.smallModel === undefined ? existing?.smallModel : undefined].filter((id) => !!id);
|
|
236
239
|
modelIds = [...new Set([...modelIds, ...retained])];
|
|
@@ -242,7 +245,7 @@ export async function cmdAdd(opts) {
|
|
|
242
245
|
fail(t("add.atLeastOne"));
|
|
243
246
|
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog());
|
|
244
247
|
const hint = opts.modelsDev ?? existing?.modelsDevId ?? guessProviderHint(catalog, answers.baseUrl);
|
|
245
|
-
const models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, modelIds, { ...metadataOptions, modelsDevId: hint, models: existing?.models }));
|
|
248
|
+
const models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, modelIds, { ...metadataOptions, modelsDevId: hint, models: existing?.models }, { endpointLimits }));
|
|
246
249
|
const matched = models.filter((m) => m.contextWindow !== undefined).length;
|
|
247
250
|
const defaultModel = opts.defaultModel ?? existing?.defaultModel ?? modelIds[0];
|
|
248
251
|
if (!modelIds.includes(defaultModel))
|
|
@@ -287,7 +290,10 @@ async function createProvider(opts) {
|
|
|
287
290
|
const metadataOptions = resolveMetadataOptions(opts, opts.existing);
|
|
288
291
|
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog());
|
|
289
292
|
const hint = opts.modelsDev ?? opts.existing?.modelsDevId ?? guessProviderHint(catalog, baseUrl);
|
|
290
|
-
const models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, modelIds, { ...metadataOptions, modelsDevId: hint, models: opts.existing?.models }, {
|
|
293
|
+
const models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, modelIds, { ...metadataOptions, modelsDevId: hint, models: opts.existing?.models }, {
|
|
294
|
+
gatewayLoader: opts.gatewayLoader,
|
|
295
|
+
endpointLimits: opts.endpointLimits,
|
|
296
|
+
}));
|
|
291
297
|
const matched = models.filter((m) => m.contextWindow !== undefined).length;
|
|
292
298
|
const defaultModel = opts.defaultModel ?? opts.existing?.defaultModel ?? modelIds[0];
|
|
293
299
|
if (!modelIds.includes(defaultModel))
|
|
@@ -387,12 +393,12 @@ export async function cmdQuickAdd(opts) {
|
|
|
387
393
|
const id = byId?.id ?? sameAccount?.id ?? explicitId ?? availableProviderId(providerIdFromBaseUrl(baseUrl, protocol), store.providers);
|
|
388
394
|
const name = opts.name ?? existing?.name ?? (opts.id === undefined ? providerNameFromBaseUrl(baseUrl, protocol) : multi ? `${opts.id} (${protocol})` : opts.id);
|
|
389
395
|
const modelFilter = parseFilterOpts(opts, existing?.modelFilter);
|
|
390
|
-
const discovered = await withProgress(`${t("add.discovering", { url: baseUrl })} [${protocol}]`, (progress) =>
|
|
396
|
+
const discovered = await withProgress(`${t("add.discovering", { url: baseUrl })} [${protocol}]`, (progress) => discoverProviderCatalog({ baseUrl, apiKey, protocol }, {
|
|
391
397
|
onPage: ({ page, count }) => progress.update(`${t("progress.discoverPage", { url: baseUrl, page, count })} [${protocol}]`),
|
|
392
398
|
}));
|
|
393
|
-
out(t("add.providerLists", { count: discovered.length }));
|
|
399
|
+
out(t("add.providerLists", { count: discovered.ids.length }));
|
|
394
400
|
const retained = [opts.defaultModel === undefined ? existing?.defaultModel : undefined, opts.smallModel === undefined ? existing?.smallModel : undefined].filter((id) => !!id);
|
|
395
|
-
let modelIds = [...new Set([...discovered, ...retained])];
|
|
401
|
+
let modelIds = [...new Set([...discovered.ids, ...retained])];
|
|
396
402
|
const pinned = [...retained, ...(opts.defaultModel ? [opts.defaultModel] : []), ...(opts.smallModel ? [opts.smallModel] : [])];
|
|
397
403
|
const outcome = applyModelFilter(modelIds, modelFilter, pinned);
|
|
398
404
|
reportDropped(outcome.dropped);
|
|
@@ -420,6 +426,7 @@ export async function cmdQuickAdd(opts) {
|
|
|
420
426
|
gatewayModels: opts.gatewayModels,
|
|
421
427
|
metadataMode: opts.metadataMode,
|
|
422
428
|
gatewayLoader,
|
|
429
|
+
endpointLimits: discovered.limits,
|
|
423
430
|
});
|
|
424
431
|
createdIds.push(provider.id);
|
|
425
432
|
note(`\n${t("add.next", { id: provider.id })}`);
|
|
@@ -466,13 +473,14 @@ export async function cmdPrune(id, opts) {
|
|
|
466
473
|
out(`${t("remove.pruning", { id: pc.bold(id) })}\n`);
|
|
467
474
|
const results = await runTargets("prune", provider, opts.apps);
|
|
468
475
|
if (isJson()) {
|
|
476
|
+
const failed = results.filter((r) => r.failed).map((r) => r.app);
|
|
469
477
|
emitJson({
|
|
470
478
|
provider: id,
|
|
471
479
|
op: "prune",
|
|
472
480
|
apps: results.filter((r) => !r.skipped).map((r) => r.app),
|
|
473
481
|
files: results.flatMap((r) => (r.skipped ? [] : r.changed)),
|
|
474
482
|
skipped: results.filter((r) => r.skipped).map((r) => ({ app: r.app, reason: r.skipped })),
|
|
475
|
-
});
|
|
483
|
+
}, failed.length ? t("targets.failed", { apps: failed.join(", ") }) : undefined);
|
|
476
484
|
return;
|
|
477
485
|
}
|
|
478
486
|
reportResults(results);
|
|
@@ -522,11 +530,12 @@ async function runTargets(op, provider, appsFilter, opts = {}) {
|
|
|
522
530
|
results.push({ app: target.id, changed: [], notes: [], skipped: `${target.name} not detected (pass --apps ${target.id} to force)` });
|
|
523
531
|
continue;
|
|
524
532
|
}
|
|
525
|
-
results.push(await target
|
|
533
|
+
results.push(await (op === "apply" ? target.apply(provider, opts.applyOptions) : target.prune(provider)));
|
|
526
534
|
}
|
|
527
535
|
catch (err) {
|
|
528
|
-
const safe = opts.redactErrors === true && !(err instanceof SafeConfigError);
|
|
529
|
-
|
|
536
|
+
const safe = (opts.redactErrors === true || isJson()) && !(err instanceof SafeConfigError);
|
|
537
|
+
const message = safe ? t(opts.redactErrors ? "preview.failedSafely" : "targets.failedSafely") : err.message;
|
|
538
|
+
results.push({ app: target.id, changed: [], notes: [], skipped: pc.red(`failed: ${message}`), failed: true });
|
|
530
539
|
process.exitCode = 1;
|
|
531
540
|
}
|
|
532
541
|
}
|
|
@@ -644,20 +653,22 @@ function printFileDiff(file, next, apiKey) {
|
|
|
644
653
|
if (oldText === newText)
|
|
645
654
|
out(pc.dim(t("preview.onlyRedacted")));
|
|
646
655
|
}
|
|
647
|
-
async function runWithOptionalDryRun(op, provider, apps, dryRun, oneSlot) {
|
|
656
|
+
async function runWithOptionalDryRun(op, provider, apps, dryRun, oneSlot, applyOptions) {
|
|
648
657
|
const summarize = (results, files) => ({
|
|
658
|
+
...(results.some((r) => r.failed) ? { failed: results.filter((r) => r.failed).map((r) => r.app) } : {}),
|
|
649
659
|
apps: results.map((r) => r.app),
|
|
650
660
|
files,
|
|
651
661
|
skipped: results.filter((r) => r.skipped).map((r) => ({ app: r.app, reason: r.skipped })),
|
|
652
662
|
});
|
|
653
663
|
if (!dryRun) {
|
|
654
|
-
const results = await runTargets(op, provider, apps, { oneSlot });
|
|
664
|
+
const results = await runTargets(op, provider, apps, { oneSlot, applyOptions });
|
|
655
665
|
reportResults(results);
|
|
656
666
|
return summarize(results, results.flatMap((r) => (r.skipped ? [] : r.changed)));
|
|
657
667
|
}
|
|
668
|
+
const wasDryRun = isDryRun();
|
|
658
669
|
setDryRun(true);
|
|
659
670
|
try {
|
|
660
|
-
const results = await runTargets(op, provider, apps, { redactErrors: true, oneSlot });
|
|
671
|
+
const results = await runTargets(op, provider, apps, { redactErrors: true, oneSlot, applyOptions });
|
|
661
672
|
const writes = drainPendingWrites();
|
|
662
673
|
for (const r of results) {
|
|
663
674
|
if (r.skipped)
|
|
@@ -670,7 +681,7 @@ async function runWithOptionalDryRun(op, provider, apps, dryRun, oneSlot) {
|
|
|
670
681
|
return summarize(results, writes.map((w) => w.file));
|
|
671
682
|
}
|
|
672
683
|
finally {
|
|
673
|
-
setDryRun(
|
|
684
|
+
setDryRun(wasDryRun);
|
|
674
685
|
}
|
|
675
686
|
}
|
|
676
687
|
export async function cmdUse(id, opts) {
|
|
@@ -703,7 +714,18 @@ export async function cmdUse(id, opts) {
|
|
|
703
714
|
out(`${t("use.switching", { id: pc.bold(id), protocol: provider.protocol, model: provider.defaultModel })}\n`);
|
|
704
715
|
const outcome = await runWithOptionalDryRun("apply", provider, opts.apps, opts.dryRun);
|
|
705
716
|
if (isJson()) {
|
|
706
|
-
emitJson({ provider: id, model: provider.defaultModel, dryRun: opts.dryRun === true, ...outcome });
|
|
717
|
+
emitJson({ provider: id, model: provider.defaultModel, dryRun: opts.dryRun === true, ...outcome }, outcome.failed?.length ? t("targets.failed", { apps: outcome.failed.join(", ") }) : undefined);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function saveRefreshedProvider(store, provider) {
|
|
721
|
+
const previous = getProvider(store, provider.id);
|
|
722
|
+
store.providers[provider.id] = provider;
|
|
723
|
+
try {
|
|
724
|
+
saveStore(store);
|
|
725
|
+
}
|
|
726
|
+
catch (err) {
|
|
727
|
+
store.providers[provider.id] = previous;
|
|
728
|
+
throw err;
|
|
707
729
|
}
|
|
708
730
|
}
|
|
709
731
|
/**
|
|
@@ -713,11 +735,11 @@ export async function cmdUse(id, opts) {
|
|
|
713
735
|
* real sync would write without leaving the fetched list behind in the store.
|
|
714
736
|
*/
|
|
715
737
|
async function refreshProviderModels(store, provider, save = true) {
|
|
716
|
-
const listed = await withProgress(t("add.discovering", { url: provider.baseUrl }), (progress) =>
|
|
738
|
+
const listed = await withProgress(t("add.discovering", { url: provider.baseUrl }), (progress) => discoverProviderCatalog(provider, {
|
|
717
739
|
onPage: ({ page, count }) => progress.update(t("progress.discoverPage", { url: provider.baseUrl, page, count })),
|
|
718
740
|
}));
|
|
719
741
|
const pinned = [provider.defaultModel, provider.smallModel].filter((model) => !!model);
|
|
720
|
-
const outcome = applyModelFilter(listed, provider.modelFilter, pinned);
|
|
742
|
+
const outcome = applyModelFilter(listed.ids, provider.modelFilter, pinned);
|
|
721
743
|
reportDropped(outcome.dropped);
|
|
722
744
|
const ids = outcome.kept;
|
|
723
745
|
const retained = new Set([...ids, ...pinned]);
|
|
@@ -725,12 +747,12 @@ async function refreshProviderModels(store, provider, save = true) {
|
|
|
725
747
|
const added = ids.filter((m) => !known.includes(m));
|
|
726
748
|
const removed = known.filter((m) => !retained.has(m));
|
|
727
749
|
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) }));
|
|
750
|
+
provider.models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, [...retained], { ...provider, modelsDevId: provider.modelsDevId ?? guessProviderHint(catalog, provider.baseUrl) }, { endpointLimits: listed.limits }));
|
|
729
751
|
// Reported by the caller, where it reads after the table or the per-app results instead of
|
|
730
752
|
// scrolling away above them.
|
|
731
753
|
const missing = [...new Set(pinned)].filter((model) => !ids.includes(model));
|
|
732
754
|
if (save)
|
|
733
|
-
|
|
755
|
+
saveRefreshedProvider(store, provider);
|
|
734
756
|
return { ids, added, removed, missing, catalog };
|
|
735
757
|
}
|
|
736
758
|
function printModelRefresh(provider, refresh) {
|
|
@@ -817,32 +839,51 @@ function printMultiSummary(count, failed, key) {
|
|
|
817
839
|
? pc.yellow(t(key === "sync" ? "sync.summaryFailed" : "discover.summaryFailed", { ok: done, count }))
|
|
818
840
|
: t(key === "sync" ? "sync.summaryOk" : "discover.summaryOk", { count })}`);
|
|
819
841
|
}
|
|
820
|
-
|
|
821
|
-
const
|
|
822
|
-
|
|
842
|
+
function syncTargets(opts) {
|
|
843
|
+
const selected = resolveTargets(opts.apps);
|
|
844
|
+
if (opts.keepDefault) {
|
|
845
|
+
const unsupported = selected.filter((target) => !target.supportsKeepDefault);
|
|
846
|
+
if (unsupported.length)
|
|
847
|
+
fail(t("sync.keepDefaultUnsupported", { apps: unsupported.map((target) => target.id).join(", ") }));
|
|
848
|
+
}
|
|
849
|
+
return selected;
|
|
850
|
+
}
|
|
851
|
+
async function syncProviders(opts, sourceStore) {
|
|
852
|
+
const selectedTargets = syncTargets(opts);
|
|
853
|
+
const store = sourceStore ?? loadStore();
|
|
823
854
|
const ids = activeLast(store, resolveProviderIds(store, opts, "active"));
|
|
824
855
|
const many = ids.length > 1;
|
|
825
856
|
const oneSlot = many ? { winners: oneSlotWinners(selectedTargets, store, ids) } : undefined;
|
|
826
857
|
const saveRefresh = opts.dryRun !== true;
|
|
827
858
|
const reports = [];
|
|
828
|
-
|
|
859
|
+
const failed = [];
|
|
829
860
|
for (const [index, id] of ids.entries()) {
|
|
830
|
-
const provider = getProvider(store, id);
|
|
861
|
+
const provider = structuredClone(getProvider(store, id));
|
|
831
862
|
if (index)
|
|
832
863
|
out("");
|
|
833
864
|
let refresh;
|
|
834
865
|
if (opts.refresh !== false) {
|
|
835
866
|
try {
|
|
836
|
-
|
|
867
|
+
const wasDryRun = isDryRun();
|
|
868
|
+
try {
|
|
869
|
+
if (opts.dryRun)
|
|
870
|
+
setDryRun(true);
|
|
871
|
+
refresh = await refreshProviderModels(store, provider, saveRefresh);
|
|
872
|
+
}
|
|
873
|
+
finally {
|
|
874
|
+
// Metadata cache writes are not target configuration previews.
|
|
875
|
+
if (opts.dryRun)
|
|
876
|
+
setDryRun(wasDryRun);
|
|
877
|
+
}
|
|
837
878
|
}
|
|
838
879
|
catch (err) {
|
|
839
880
|
// One unreachable provider must not strand the others: report it, write nothing for it,
|
|
840
|
-
// and fail the run.
|
|
841
|
-
if (!many)
|
|
881
|
+
// and fail the run. Text mode keeps the single-provider exception contract.
|
|
882
|
+
if (!many && !isJson())
|
|
842
883
|
throw new Error(`${err.message}\n${t("sync.refreshFailed")}`);
|
|
843
884
|
warn(t("sync.refreshSkipped", { id, message: err.message }));
|
|
844
885
|
process.exitCode = 1;
|
|
845
|
-
failed
|
|
886
|
+
failed.push(id);
|
|
846
887
|
reports.push({ provider: id, refreshFailed: true });
|
|
847
888
|
continue;
|
|
848
889
|
}
|
|
@@ -853,8 +894,10 @@ export async function cmdSync(opts) {
|
|
|
853
894
|
note(t("sync.refreshNotSaved"));
|
|
854
895
|
}
|
|
855
896
|
}
|
|
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);
|
|
897
|
+
out(`${t(opts.keepDefault ? "sync.syncingKeepDefault" : "sync.syncing", { id: pc.bold(id), model: provider.defaultModel })}\n`);
|
|
898
|
+
const outcome = await runWithOptionalDryRun("apply", provider, opts.apps, opts.dryRun, oneSlot, { keepDefault: opts.keepDefault });
|
|
899
|
+
if (outcome.failed?.length)
|
|
900
|
+
failed.push(id);
|
|
858
901
|
if (refresh?.missing.length) {
|
|
859
902
|
const models = refresh.missing.join(", ");
|
|
860
903
|
warn(t(opts.dryRun ? "refresh.pinnedMissing" : "sync.pinnedMissingWritten", { models, id }));
|
|
@@ -876,19 +919,21 @@ export async function cmdSync(opts) {
|
|
|
876
919
|
...outcome,
|
|
877
920
|
});
|
|
878
921
|
}
|
|
922
|
+
if (many)
|
|
923
|
+
printMultiSummary(ids.length, failed.length, "sync");
|
|
924
|
+
return { reports, failed };
|
|
925
|
+
}
|
|
926
|
+
export async function cmdSync(opts) {
|
|
927
|
+
const { reports, failed } = await syncProviders(opts);
|
|
879
928
|
if (isJson()) {
|
|
880
|
-
|
|
881
|
-
emitJson(many
|
|
929
|
+
emitJson(reports.length > 1
|
|
882
930
|
? {
|
|
883
931
|
dryRun: opts.dryRun === true,
|
|
884
932
|
providers: reports,
|
|
885
933
|
files: [...new Set(reports.flatMap((report) => report.files ?? []))],
|
|
886
934
|
}
|
|
887
|
-
: { ...
|
|
888
|
-
return;
|
|
935
|
+
: { ...(reports[0] ?? {}), dryRun: opts.dryRun === true }, failed.length ? t("sync.failed", { ids: failed.join(", ") }) : undefined);
|
|
889
936
|
}
|
|
890
|
-
if (many)
|
|
891
|
-
printMultiSummary(ids.length, failed, "sync");
|
|
892
937
|
}
|
|
893
938
|
export function cmdStatus() {
|
|
894
939
|
const store = loadStore();
|
|
@@ -995,7 +1040,9 @@ export async function cmdRefreshMeta(opts = {}) {
|
|
|
995
1040
|
printUncatalogedHint(provider.models, catalog, provider.modelsDevId);
|
|
996
1041
|
}
|
|
997
1042
|
export async function cmdDiscover(ids, opts) {
|
|
998
|
-
|
|
1043
|
+
if (opts.keepDefault && !opts.sync)
|
|
1044
|
+
fail(t("discover.keepDefaultRequiresSync"));
|
|
1045
|
+
syncTargets(opts);
|
|
999
1046
|
const store = loadStore();
|
|
1000
1047
|
// Ids may arrive as positional arguments, as one comma-separated list, or through `-p` for
|
|
1001
1048
|
// symmetry with sync; all three say the same thing, so accept them together.
|
|
@@ -1006,8 +1053,9 @@ export async function cmdDiscover(ids, opts) {
|
|
|
1006
1053
|
const many = selected.length > 1;
|
|
1007
1054
|
const reports = [];
|
|
1008
1055
|
const refreshed = [];
|
|
1056
|
+
const failed = [];
|
|
1009
1057
|
for (const [index, id] of selected.entries()) {
|
|
1010
|
-
const provider = getProvider(store, id);
|
|
1058
|
+
const provider = structuredClone(getProvider(store, id));
|
|
1011
1059
|
if (index)
|
|
1012
1060
|
out("");
|
|
1013
1061
|
Object.assign(provider, resolveMetadataOptions(opts, provider));
|
|
@@ -1019,14 +1067,28 @@ export async function cmdDiscover(ids, opts) {
|
|
|
1019
1067
|
provider.modelFilter = flagFilter;
|
|
1020
1068
|
let refresh;
|
|
1021
1069
|
try {
|
|
1022
|
-
|
|
1070
|
+
const wasDryRun = isDryRun();
|
|
1071
|
+
try {
|
|
1072
|
+
if (opts.dryRun)
|
|
1073
|
+
setDryRun(true);
|
|
1074
|
+
refresh = await refreshProviderModels(store, provider, false);
|
|
1075
|
+
if (opts.dryRun)
|
|
1076
|
+
store.providers[id] = provider;
|
|
1077
|
+
else
|
|
1078
|
+
saveRefreshedProvider(store, provider);
|
|
1079
|
+
}
|
|
1080
|
+
finally {
|
|
1081
|
+
if (opts.dryRun)
|
|
1082
|
+
setDryRun(wasDryRun);
|
|
1083
|
+
}
|
|
1023
1084
|
}
|
|
1024
1085
|
catch (err) {
|
|
1025
|
-
// One unreachable endpoint must not strand the others;
|
|
1026
|
-
if (!many)
|
|
1086
|
+
// One unreachable endpoint must not strand the others; text mode still throws for one.
|
|
1087
|
+
if (!many && !isJson())
|
|
1027
1088
|
throw err;
|
|
1028
1089
|
warn(t("sync.refreshSkipped", { id, message: err.message }));
|
|
1029
1090
|
process.exitCode = 1;
|
|
1091
|
+
failed.push(id);
|
|
1030
1092
|
reports.push({ provider: id, refreshFailed: true });
|
|
1031
1093
|
continue;
|
|
1032
1094
|
}
|
|
@@ -1038,7 +1100,17 @@ export async function cmdDiscover(ids, opts) {
|
|
|
1038
1100
|
warn(t("refresh.pinnedMissing", { models: refresh.missing.join(", "), id }));
|
|
1039
1101
|
}
|
|
1040
1102
|
if (isJson()) {
|
|
1041
|
-
|
|
1103
|
+
if (opts.sync && refreshed.length) {
|
|
1104
|
+
// Collect application results without emitting or fetching the discovered lists again.
|
|
1105
|
+
const synced = await syncProviders({ provider: refreshed.join(","), apps: opts.apps, refresh: false, keepDefault: opts.keepDefault, dryRun: opts.dryRun }, store);
|
|
1106
|
+
failed.push(...synced.failed);
|
|
1107
|
+
for (const report of reports)
|
|
1108
|
+
Object.assign(report, synced.reports.find((entry) => entry.provider === report.provider));
|
|
1109
|
+
}
|
|
1110
|
+
const files = [...new Set(reports.flatMap((report) => report.files ?? []))];
|
|
1111
|
+
emitJson(many
|
|
1112
|
+
? { providers: reports, ...(opts.sync ? { files, dryRun: opts.dryRun === true } : {}) }
|
|
1113
|
+
: { ...(reports[0] ?? {}), ...(opts.sync ? { dryRun: opts.dryRun === true } : {}) }, failed.length ? t("discover.failed", { ids: failed.join(", ") }) : undefined);
|
|
1042
1114
|
return;
|
|
1043
1115
|
}
|
|
1044
1116
|
if (many)
|
|
@@ -1046,7 +1118,7 @@ export async function cmdDiscover(ids, opts) {
|
|
|
1046
1118
|
if (opts.sync && refreshed.length) {
|
|
1047
1119
|
out("");
|
|
1048
1120
|
// The lists are already fresh: write them without a second fetch.
|
|
1049
|
-
await
|
|
1121
|
+
await syncProviders({ provider: refreshed.join(","), apps: opts.apps, refresh: false, keepDefault: opts.keepDefault, dryRun: opts.dryRun }, store);
|
|
1050
1122
|
}
|
|
1051
1123
|
else if (!opts.sync) {
|
|
1052
1124
|
note(`\n${t("discover.next")}`);
|
|
@@ -1309,8 +1381,9 @@ export async function cmdImport(opts) {
|
|
|
1309
1381
|
continue;
|
|
1310
1382
|
}
|
|
1311
1383
|
let ids = [...c.models];
|
|
1384
|
+
let endpointLimits = new Map();
|
|
1312
1385
|
if (ids.length === 0) {
|
|
1313
|
-
const discover = () => withProgress(t("import.discovering", { id: c.id }), (progress) =>
|
|
1386
|
+
const discover = () => withProgress(t("import.discovering", { id: c.id }), (progress) => discoverProviderCatalog({ baseUrl: c.baseUrl, apiKey: apiKey, protocol: c.protocol }, {
|
|
1314
1387
|
onPage: ({ page, count }) => progress.update(t("progress.discoverPage", { url: c.baseUrl, page, count })),
|
|
1315
1388
|
}));
|
|
1316
1389
|
let listed;
|
|
@@ -1334,14 +1407,15 @@ export async function cmdImport(opts) {
|
|
|
1334
1407
|
out(pc.yellow(t("import.skippedAfterFailure", { id: c.id })));
|
|
1335
1408
|
continue;
|
|
1336
1409
|
}
|
|
1337
|
-
ids = listed;
|
|
1410
|
+
ids = listed.ids;
|
|
1411
|
+
endpointLimits = listed.limits;
|
|
1338
1412
|
ids = applyModelFilter(ids, undefined, []).kept;
|
|
1339
1413
|
}
|
|
1340
1414
|
if (ids.length === 0)
|
|
1341
1415
|
fail(t("import.noModelsImport", { id: c.id }));
|
|
1342
1416
|
const id = availableProviderId(c.id, store.providers);
|
|
1343
1417
|
const hint = guessProviderHint(catalog, c.baseUrl);
|
|
1344
|
-
const models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, ids, { ...metadataOptions, modelsDevId: hint }, { gatewayLoader }));
|
|
1418
|
+
const models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, ids, { ...metadataOptions, modelsDevId: hint }, { gatewayLoader, endpointLimits }));
|
|
1345
1419
|
const defaultModel = c.defaultModel && ids.includes(c.defaultModel) ? c.defaultModel : ids[0];
|
|
1346
1420
|
const existed = store.providers[id] !== undefined;
|
|
1347
1421
|
store.providers[id] = {
|
package/dist/discover.js
CHANGED
|
@@ -13,6 +13,32 @@ function modelListUrl(baseUrl) {
|
|
|
13
13
|
}
|
|
14
14
|
/** Hard cap on model-list pages; an endpoint emitting endless fresh cursors must not loop forever. */
|
|
15
15
|
export const MAX_MODEL_PAGES = 100;
|
|
16
|
+
/**
|
|
17
|
+
* Spellings endpoints use for the same limit, in priority order: the first
|
|
18
|
+
* declared key wins when a row carries several. Providers do not agree on
|
|
19
|
+
* names — an OpenAI-compatible gateway publishes context_window_tokens /
|
|
20
|
+
* maximum_output_tokens, OpenRouter publishes context_length /
|
|
21
|
+
* max_output_tokens — and a row without the canonical name would otherwise
|
|
22
|
+
* contribute nothing.
|
|
23
|
+
*/
|
|
24
|
+
const contextWindowKeys = ["context_window_tokens", "context_length", "max_context_length"];
|
|
25
|
+
const maxOutputKeys = ["maximum_output_tokens", "max_output_tokens", "max_completion_tokens"];
|
|
26
|
+
function declaredLimit(row, keys) {
|
|
27
|
+
for (const key of keys) {
|
|
28
|
+
const value = row[key];
|
|
29
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0)
|
|
30
|
+
return Math.floor(value);
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/** Limits a model row advertises, or undefined when it describes nothing but its id. */
|
|
35
|
+
export function parseEndpointLimits(row) {
|
|
36
|
+
const contextWindow = declaredLimit(row, contextWindowKeys);
|
|
37
|
+
const maxOutput = declaredLimit(row, maxOutputKeys);
|
|
38
|
+
if (contextWindow === undefined && maxOutput === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
return { ...(contextWindow !== undefined ? { contextWindow } : {}), ...(maxOutput !== undefined ? { maxOutput } : {}) };
|
|
41
|
+
}
|
|
16
42
|
function parseModelPage(body) {
|
|
17
43
|
if (!body || typeof body !== "object")
|
|
18
44
|
throw new Error("invalid model-list response");
|
|
@@ -22,25 +48,36 @@ function parseModelPage(body) {
|
|
|
22
48
|
throw new Error("model-list response has no model array");
|
|
23
49
|
if (page.has_more !== undefined && typeof page.has_more !== "boolean")
|
|
24
50
|
throw new Error("invalid model-list pagination flag");
|
|
25
|
-
const ids =
|
|
51
|
+
const ids = [];
|
|
52
|
+
const limits = new Map();
|
|
53
|
+
for (const row of rows) {
|
|
26
54
|
if (!row || typeof row !== "object")
|
|
27
|
-
|
|
55
|
+
continue;
|
|
28
56
|
const id = row.id;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
57
|
+
if (typeof id !== "string" || id.length === 0)
|
|
58
|
+
continue;
|
|
59
|
+
ids.push(id);
|
|
60
|
+
const declared = parseEndpointLimits(row);
|
|
61
|
+
if (declared)
|
|
62
|
+
limits.set(id, declared);
|
|
63
|
+
}
|
|
64
|
+
return { ids, limits, hasMore: page.has_more === true, lastId: typeof page.last_id === "string" ? page.last_id : undefined };
|
|
32
65
|
}
|
|
33
66
|
/**
|
|
34
|
-
* List
|
|
35
|
-
*
|
|
36
|
-
* metadata is filled from models.dev afterwards
|
|
67
|
+
* List the provider's own /v1/models catalog: model ids plus the limits a row
|
|
68
|
+
* declares for itself. Plenty of resellers expose bare ids — those ids carry no
|
|
69
|
+
* entry in `limits` and metadata is filled from models.dev afterwards
|
|
70
|
+
* (enrichModels). An endpoint that does declare limits is authoritative for its
|
|
71
|
+
* own route, so those numbers outrank the catalogs instead of being replaced by
|
|
72
|
+
* whatever a third party lists for the same model.
|
|
37
73
|
*/
|
|
38
|
-
export async function
|
|
74
|
+
export async function discoverProviderCatalog(opts, hooks = {}) {
|
|
39
75
|
const url = modelListUrl(opts.baseUrl);
|
|
40
76
|
const headers = opts.protocol === "anthropic"
|
|
41
77
|
? { "x-api-key": opts.apiKey, "anthropic-version": "2023-06-01" }
|
|
42
78
|
: { authorization: `Bearer ${opts.apiKey}` };
|
|
43
79
|
const ids = new Set();
|
|
80
|
+
const limits = new Map();
|
|
44
81
|
const cursors = new Set();
|
|
45
82
|
const initialCursor = url.searchParams.get("after_id");
|
|
46
83
|
if (initialCursor)
|
|
@@ -55,6 +92,9 @@ export async function discoverProviderModels(opts, hooks = {}) {
|
|
|
55
92
|
const parsed = parseModelPage(await res.json());
|
|
56
93
|
for (const id of parsed.ids)
|
|
57
94
|
ids.add(id);
|
|
95
|
+
for (const [id, declared] of parsed.limits)
|
|
96
|
+
if (ids.has(id))
|
|
97
|
+
limits.set(id, declared);
|
|
58
98
|
hooks.onPage?.({ page, count: ids.size });
|
|
59
99
|
if (!parsed.hasMore)
|
|
60
100
|
break;
|
|
@@ -67,7 +107,11 @@ export async function discoverProviderModels(opts, hooks = {}) {
|
|
|
67
107
|
}
|
|
68
108
|
if (ids.size === 0)
|
|
69
109
|
throw new Error("provider returned no models");
|
|
70
|
-
return [...ids].sort();
|
|
110
|
+
return { ids: [...ids].sort(), limits };
|
|
111
|
+
}
|
|
112
|
+
/** The listed ids alone, for callers that only need the model list. */
|
|
113
|
+
export async function discoverProviderModels(opts, hooks = {}) {
|
|
114
|
+
return (await discoverProviderCatalog(opts, hooks)).ids;
|
|
71
115
|
}
|
|
72
116
|
/**
|
|
73
117
|
* Probe which wire protocols an endpoint supports by hitting /v1/models
|
package/dist/i18n.js
CHANGED
|
@@ -63,6 +63,11 @@ const messages = {
|
|
|
63
63
|
"opt.showProvider": { en: "show models of a configured provider", "zh-CN": "显示已配置供应商的模型" },
|
|
64
64
|
"opt.refresh": { en: "force-refresh models.dev cache", "zh-CN": "强制刷新 models.dev 缓存" },
|
|
65
65
|
"opt.noRefresh": { en: "skip the model-list refresh; write the saved list only", "zh-CN": "跳过模型列表刷新;只写入已保存的列表" },
|
|
66
|
+
"opt.keepDefault": { en: "sync providers/models only; leave app settings and default selection untouched (pi/prime only; discover requires --sync)", "zh-CN": "仅同步供应商/模型,保持应用设置和默认选择不变(仅 pi/prime;discover 需搭配 --sync)" },
|
|
67
|
+
"sync.keepDefaultUnsupported": { en: "--keep-default is not supported by: {apps}; select only --apps pi,prime", "zh-CN": "以下应用不支持 --keep-default:{apps};请仅选择 --apps pi,prime" },
|
|
68
|
+
"discover.keepDefaultRequiresSync": { en: "discover --keep-default requires --sync", "zh-CN": "discover --keep-default 需搭配 --sync" },
|
|
69
|
+
"sync.keepDefaultMissing": { en: "{file}: --keep-default stopped: current default {id}/{model} is missing from the synchronized model list; keep this model explicitly pinned or select an available default before retrying. No app files were changed.", "zh-CN": "{file}:--keep-default 已停止:当前默认 {id}/{model} 未出现在同步模型列表中;请明确保留此模型,或先选择可用默认模型再重试。应用文件未修改。" },
|
|
70
|
+
"sync.syncingKeepDefault": { en: "syncing {id} providers/models; app defaults stay unchanged", "zh-CN": "同步 {id} 的供应商/模型;应用默认选择保持不变" },
|
|
66
71
|
"opt.limit": { en: "maximum results", "zh-CN": "最大结果数" },
|
|
67
72
|
"opt.syncAfter": { en: "push refreshed provider into app configs", "zh-CN": "刷新后推送到应用配置" },
|
|
68
73
|
"opt.appsSync": { en: "apps to sync when --sync is set", "zh-CN": "--sync 时要同步的应用" },
|
|
@@ -248,9 +253,12 @@ const messages = {
|
|
|
248
253
|
"select.noneSelected": { en: "the provider selection is empty; name at least one id", "zh-CN": "未指定任何供应商,至少需要一个 id" },
|
|
249
254
|
"sync.refreshSkipped": { en: "skipping {id}: {message}", "zh-CN": "已跳过 {id}:{message}" },
|
|
250
255
|
"sync.summaryOk": { en: "synced {count} providers", "zh-CN": "已同步 {count} 个供应商" },
|
|
251
|
-
"sync.summaryFailed": { en: "synced {ok} of {count} providers; the rest
|
|
256
|
+
"sync.summaryFailed": { en: "synced {ok} of {count} providers; the rest failed", "zh-CN": "已同步 {count} 个中的 {ok} 个供应商,其余失败" },
|
|
252
257
|
"sync.refreshNotSaved": { en: "dry run: the refreshed model list was not saved", "zh-CN": "试运行:刷新到的模型列表未保存" },
|
|
253
258
|
"sync.refreshFailed": { en: "could not refresh the model list; pass --no-refresh to write the saved list instead", "zh-CN": "无法刷新模型列表;可以加 --no-refresh 只写入已保存的列表" },
|
|
259
|
+
"sync.failed": { en: "sync failed for providers: {ids}", "zh-CN": "供应商同步失败:{ids}" },
|
|
260
|
+
"targets.failed": { en: "failed targets: {apps}", "zh-CN": "目标应用失败:{apps}" },
|
|
261
|
+
"targets.failedSafely": { en: "target operation failed", "zh-CN": "目标应用操作失败" },
|
|
254
262
|
"status.config": { en: "config: {file}", "zh-CN": "配置:{file}" },
|
|
255
263
|
"status.active": { en: "active provider: {id}", "zh-CN": "当前供应商:{id}" },
|
|
256
264
|
"status.none": { en: "(none)", "zh-CN": "(无)" },
|
|
@@ -387,6 +395,7 @@ const messages = {
|
|
|
387
395
|
"upgrade.failed": { en: "{id} upgrade failed: {message}", "zh-CN": "{id} 升级失败:{message}" },
|
|
388
396
|
"discover.summaryOk": { en: "refreshed {count} providers", "zh-CN": "已刷新 {count} 个供应商" },
|
|
389
397
|
"discover.summaryFailed": { en: "refreshed {ok} of {count} providers; the rest were skipped", "zh-CN": "已刷新 {count} 个中的 {ok} 个供应商,其余已跳过" },
|
|
398
|
+
"discover.failed": { en: "discovery or sync failed for providers: {ids}", "zh-CN": "供应商发现或同步失败:{ids}" },
|
|
390
399
|
"refresh.pinnedMissing": {
|
|
391
400
|
en: "{models} no longer listed by the endpoint; kept as configured — pick a listed model with: agentsw use {id} -m <model>",
|
|
392
401
|
"zh-CN": "{models} 已不在端点的模型列表中;仍按原配置保留 —— 可用 agentsw use {id} -m <model> 换成列表中的模型",
|
package/dist/index.js
CHANGED
|
@@ -145,6 +145,7 @@ program
|
|
|
145
145
|
.option("--all", t("opt.syncAll"))
|
|
146
146
|
.option("-n, --dry-run", t("opt.dryRun"))
|
|
147
147
|
.option("--no-refresh", t("opt.noRefresh"))
|
|
148
|
+
.option("--keep-default", t("opt.keepDefault"))
|
|
148
149
|
.action(cmdSync);
|
|
149
150
|
program.command("status").description(t("cmd.status")).action(cmdStatus);
|
|
150
151
|
program
|
|
@@ -162,6 +163,8 @@ program
|
|
|
162
163
|
.option("--all", t("opt.refreshAll"))
|
|
163
164
|
.option("-s, --sync", t("opt.syncAfter"))
|
|
164
165
|
.option("-a, --apps <apps>", t("opt.appsSync"))
|
|
166
|
+
.option("--keep-default", t("opt.keepDefault"))
|
|
167
|
+
.option("-n, --dry-run", t("opt.dryRun"))
|
|
165
168
|
.option("--include <globs>", t("opt.setInclude"))
|
|
166
169
|
.option("--exclude <globs>", t("opt.setExclude"))
|
|
167
170
|
.option("--dedup", t("opt.dedup"))
|
package/dist/metadata.js
CHANGED
|
@@ -70,6 +70,15 @@ const fields = [
|
|
|
70
70
|
"cost.input", "cost.output", "cost.cacheRead", "cost.cacheWrite",
|
|
71
71
|
];
|
|
72
72
|
const gatewayFields = fields.filter((field) => !field.startsWith("cost."));
|
|
73
|
+
/** Limits are all an endpoint is trusted for: its listing describes its own wire, not capabilities. */
|
|
74
|
+
const endpointFields = ["contextWindow", "maxOutput"];
|
|
75
|
+
/**
|
|
76
|
+
* Which source owns a field it proposes. An endpoint that declares its own
|
|
77
|
+
* limits outranks both catalogs — models.dev describes the model, the public
|
|
78
|
+
* Gateway describes *that* gateway, and neither is the route serving the
|
|
79
|
+
* request. models.dev outranks Gateway, which only fills what it is given.
|
|
80
|
+
*/
|
|
81
|
+
const sourceRank = { endpoint: 3, "models.dev": 2, "ai-gateway": 1 };
|
|
73
82
|
const limits = ["maxInput", "maxOutput"];
|
|
74
83
|
const coreFields = ["contextWindow", "maxOutput", "reasoning", "imageInput"];
|
|
75
84
|
function fieldValue(spec, field) {
|
|
@@ -244,7 +253,9 @@ function clearStaleIdentity(spec, modelId) {
|
|
|
244
253
|
return;
|
|
245
254
|
for (const field of fields) {
|
|
246
255
|
const origin = metadata.fields?.[field];
|
|
247
|
-
|
|
256
|
+
// Endpoint limits are keyed by the provider's own id, not by a catalog row:
|
|
257
|
+
// a catalog id that differs says nothing about the route's own numbers.
|
|
258
|
+
if (!origin || origin.source === "endpoint" || origin.modelId === modelId)
|
|
248
259
|
continue;
|
|
249
260
|
if (equalValue(fieldValue(spec, field), origin.value))
|
|
250
261
|
setField(spec, field, undefined);
|
|
@@ -252,12 +263,13 @@ function clearStaleIdentity(spec, modelId) {
|
|
|
252
263
|
}
|
|
253
264
|
if (metadata.gateway && metadata.gateway.modelId !== modelId)
|
|
254
265
|
delete metadata.gateway;
|
|
255
|
-
if (metadata.conflicts)
|
|
256
|
-
metadata.conflicts = metadata.conflicts.filter((conflict) => conflict.modelId === modelId);
|
|
266
|
+
if (metadata.conflicts) {
|
|
267
|
+
metadata.conflicts = metadata.conflicts.filter((conflict) => conflict.source === "endpoint" || conflict.modelId === modelId);
|
|
268
|
+
}
|
|
257
269
|
}
|
|
258
270
|
/** Apply a source as a set of field proposals, checking bounds against retained values first. */
|
|
259
271
|
function mergeSource(spec, incoming, source, modelId, now, fetchedAt, supplementalGateway) {
|
|
260
|
-
const sourceFields = source === "ai-gateway" ? gatewayFields : fields;
|
|
272
|
+
const sourceFields = source === "endpoint" ? [...endpointFields] : source === "ai-gateway" ? gatewayFields : fields;
|
|
261
273
|
const proposals = new Map();
|
|
262
274
|
const conflicts = [];
|
|
263
275
|
const conflict = (field, value, keptValue, blockedBy) => {
|
|
@@ -272,7 +284,7 @@ function mergeSource(spec, incoming, source, modelId, now, fetchedAt, supplement
|
|
|
272
284
|
continue;
|
|
273
285
|
const current = fieldValue(spec, field);
|
|
274
286
|
const origin = spec.metadata?.fields?.[field];
|
|
275
|
-
if (current === undefined || (origin &&
|
|
287
|
+
if (current === undefined || (origin && sourceRank[source] >= sourceRank[origin.source])) {
|
|
276
288
|
proposals.set(field, value);
|
|
277
289
|
}
|
|
278
290
|
else if (!equalValue(current, value)) {
|
|
@@ -293,6 +305,18 @@ function mergeSource(spec, incoming, source, modelId, now, fetchedAt, supplement
|
|
|
293
305
|
if (!primaryFits && typeof next === "number" && next <= context)
|
|
294
306
|
retained = next;
|
|
295
307
|
if (typeof retained === "number" && retained > context) {
|
|
308
|
+
// The endpoint's own window is authoritative for its route: a retained
|
|
309
|
+
// limit it cannot hold is a catalog estimate that would otherwise pin
|
|
310
|
+
// the window open, so the limit goes and the window wins. A weaker
|
|
311
|
+
// source keeps the old outcome — the wider window stays, the proposal
|
|
312
|
+
// is reported, because nothing here proves the retained limit wrong.
|
|
313
|
+
if (source === "endpoint") {
|
|
314
|
+
conflict(field, retained, context, "contextWindow");
|
|
315
|
+
setField(spec, field, undefined);
|
|
316
|
+
if (spec.metadata?.fields)
|
|
317
|
+
delete spec.metadata.fields[field];
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
296
320
|
conflict("contextWindow", context, retained, field);
|
|
297
321
|
proposals.delete("contextWindow");
|
|
298
322
|
context = spec.contextWindow;
|
|
@@ -361,6 +385,12 @@ export async function enrichProviderModels(catalog, ids, settings, options = {})
|
|
|
361
385
|
if (origin && !equalValue(fieldValue(spec, field), origin.value))
|
|
362
386
|
delete spec.metadata.fields[field];
|
|
363
387
|
}
|
|
388
|
+
// The endpoint's own numbers go in first so the catalogs can only fill
|
|
389
|
+
// what it left unsaid; a catalog that disagrees is reported as a conflict
|
|
390
|
+
// instead of overwriting the limits the route actually enforces.
|
|
391
|
+
const declared = options.endpointLimits?.get(id);
|
|
392
|
+
if (declared)
|
|
393
|
+
mergeSource(spec, { id, ...declared }, "endpoint", id, now);
|
|
364
394
|
let primary;
|
|
365
395
|
let supplement;
|
|
366
396
|
let match;
|
|
@@ -8,8 +8,8 @@ export function transactionalTarget(target) {
|
|
|
8
8
|
const wrapped = Object.create(Object.getPrototypeOf(target), Object.getOwnPropertyDescriptors(target));
|
|
9
9
|
for (const method of ["apply", "prune"]) {
|
|
10
10
|
const operation = target[method];
|
|
11
|
-
wrapped[method] = async function (provider) {
|
|
12
|
-
const staged = await stageFileWrites(() => operation.call(this, provider));
|
|
11
|
+
wrapped[method] = async function (provider, options) {
|
|
12
|
+
const staged = await stageFileWrites(() => operation.call(this, provider, options));
|
|
13
13
|
const committed = commitFileChanges(staged.changes, { dryRun: staged.dryRun, reads: staged.reads });
|
|
14
14
|
if (staged.dryRun)
|
|
15
15
|
recordPendingWrites(staged.changes);
|
package/dist/targets/pistyle.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { backupFile, writeFileAtomic } from "../fsutil.js";
|
|
3
|
+
import { backupFile, SafeConfigError, writeFileAtomic } from "../fsutil.js";
|
|
4
4
|
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 { t } from "../i18n.js";
|
|
8
9
|
import { apiValue, applyDeepseekCompat, classifyApi, entryApi, mergeModels, OWNED_MODEL_METADATA_KEYS, ownedModelMetadata, sdkBaseUrl, stripConflictingOverrides, } from "./wire.js";
|
|
9
10
|
/** pi writes the shared metadata plus its own thinking-level map. */
|
|
10
11
|
const OWNED_MODEL_KEYS = [...OWNED_MODEL_METADATA_KEYS, "thinkingLevelMap"];
|
|
@@ -55,11 +56,12 @@ export function piStyleTarget(opts) {
|
|
|
55
56
|
id: opts.id,
|
|
56
57
|
name: opts.name,
|
|
57
58
|
protocols: ["openai", "anthropic"],
|
|
59
|
+
supportsKeepDefault: true,
|
|
58
60
|
get configPaths() {
|
|
59
61
|
return [piModelsFile(id), piSettingsFile(id)];
|
|
60
62
|
},
|
|
61
63
|
detect: () => fs.existsSync(path.dirname(resolveDir())),
|
|
62
|
-
async apply(provider) {
|
|
64
|
+
async apply(provider, options = {}) {
|
|
63
65
|
const dir = resolveDir();
|
|
64
66
|
const modelsFile = path.join(dir, "models.json");
|
|
65
67
|
const settingsFile = path.join(dir, "settings.json");
|
|
@@ -67,6 +69,11 @@ export function piStyleTarget(opts) {
|
|
|
67
69
|
// Validate both files before preparing any writes, including a malformed settings file.
|
|
68
70
|
const modelsDocument = readModels(modelsFile);
|
|
69
71
|
const settingsDocument = readSettings(settingsFile);
|
|
72
|
+
const current = settingsDocument.value;
|
|
73
|
+
if (options.keepDefault && current.defaultProvider === provider.id && current.defaultModel &&
|
|
74
|
+
!provider.models.some((model) => model.id === current.defaultModel)) {
|
|
75
|
+
throw new SafeConfigError(t("sync.keepDefaultMissing", { file: settingsFile, id: provider.id, model: current.defaultModel }));
|
|
76
|
+
}
|
|
70
77
|
const modelsConfig = { ...modelsDocument.value };
|
|
71
78
|
const providers = { ...modelsConfig.providers };
|
|
72
79
|
// Keys agentsw does not model (headers, authHeader, oauth, ...) and per-model extras
|
|
@@ -98,22 +105,25 @@ export function piStyleTarget(opts) {
|
|
|
98
105
|
models,
|
|
99
106
|
};
|
|
100
107
|
modelsConfig.providers = providers;
|
|
101
|
-
const
|
|
108
|
+
const modelsText = editJsoncObject(modelsDocument, modelsConfig);
|
|
109
|
+
const settingsText = options.keepDefault ? undefined : editJsoncObject(settingsDocument, {
|
|
102
110
|
...settingsDocument.value,
|
|
103
111
|
defaultProvider: provider.id,
|
|
104
112
|
defaultModel: provider.defaultModel,
|
|
105
|
-
};
|
|
106
|
-
const
|
|
107
|
-
const settingsText = editJsoncObject(settingsDocument, settings);
|
|
113
|
+
});
|
|
114
|
+
const changed = [modelsFile];
|
|
108
115
|
const modelsBackup = backupFile(modelsFile);
|
|
109
116
|
if (modelsBackup)
|
|
110
117
|
notes.push(`backup: ${modelsBackup}`);
|
|
111
118
|
writeFileAtomic(modelsFile, modelsText);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
if (settingsText !== undefined) {
|
|
120
|
+
const settingsBackup = backupFile(settingsFile);
|
|
121
|
+
if (settingsBackup)
|
|
122
|
+
notes.push(`backup: ${settingsBackup}`);
|
|
123
|
+
writeFileAtomic(settingsFile, settingsText);
|
|
124
|
+
changed.push(settingsFile);
|
|
125
|
+
}
|
|
126
|
+
return { app: opts.id, changed, notes };
|
|
117
127
|
},
|
|
118
128
|
async prune(provider) {
|
|
119
129
|
const dir = resolveDir();
|
package/dist/ui.js
CHANGED
|
@@ -51,10 +51,12 @@ export function warn(message) {
|
|
|
51
51
|
process.stderr.write(pc.yellow(`warning: ${message}`) + "\n");
|
|
52
52
|
}
|
|
53
53
|
function emitJsonError(message) {
|
|
54
|
-
|
|
54
|
+
emitJson(undefined, message);
|
|
55
55
|
}
|
|
56
|
-
export function emitJson(data) {
|
|
57
|
-
process.stdout.write(JSON.stringify({ ok:
|
|
56
|
+
export function emitJson(data, errorMessage) {
|
|
57
|
+
process.stdout.write(JSON.stringify({ ok: errorMessage === undefined, command, version, data,
|
|
58
|
+
...(errorMessage === undefined ? {} : { error: { message: errorMessage } }),
|
|
59
|
+
}) + "\n");
|
|
58
60
|
}
|
|
59
61
|
/** Fatal error. In machine mode the failure becomes the single stdout document, so pipes stay parseable. */
|
|
60
62
|
export function error(message) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentsw",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
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": {
|