agentsw 0.11.2 → 0.12.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 +11 -4
- package/README.zh-CN.md +7 -3
- package/dist/commands.js +15 -14
- package/dist/discover.js +64 -20
- package/dist/metadata.js +7 -7
- package/dist/targets/codex.js +13 -11
- package/dist/targets/dsh.js +4 -2
- package/dist/targets/hermes.js +5 -3
- package/dist/targets/opencode.js +11 -9
- package/dist/targets/pistyle.js +1 -1
- package/dist/targets/workbuddy.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ 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. Limits the endpoint declares for
|
|
38
|
+
`contextWindow`/`maxTokens` for omp, `reasoningEfforts`/`input` for dsh. Limits and prices the endpoint declares for
|
|
39
39
|
its own models outrank the catalogs, so one model cannot end up with two different context windows
|
|
40
40
|
depending on which provider lists it.
|
|
41
41
|
- **Adopts what you already have** — including [cc-switch](https://github.com/farion1231/cc-switch).
|
|
@@ -207,7 +207,7 @@ use `--all` explicitly in scripts. Model discovery may update the local metadata
|
|
|
207
207
|
## Model discovery filters
|
|
208
208
|
|
|
209
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
|
|
210
|
+
with automatic AI Gateway supplementation when needed. Limits and prices the provider declares for its own models in
|
|
211
211
|
that same listing win over what a catalog claims for them; ids it lists bare are enriched as before. Reseller lists are noisy,
|
|
212
212
|
so filters are persisted per provider and re-applied on every `discover`:
|
|
213
213
|
|
|
@@ -283,7 +283,12 @@ Import still skips already configured accounts.
|
|
|
283
283
|
- **Field-level priority:** manual values > the endpoint's own declaration > models.dev > Gateway gap filling.
|
|
284
284
|
An endpoint that advertises `context_window_tokens`/`maximum_output_tokens`
|
|
285
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.
|
|
286
|
+
route, so those limits are written instead of a catalog's estimate for the same model. The same applies
|
|
287
|
+
to prices a route publishes for its own models — `input_cost_per_million_tokens_usd`,
|
|
288
|
+
`output_cost_per_million_tokens_usd`, `cached_input_cost_per_million_tokens_usd`,
|
|
289
|
+
`cache_write_cost_per_million_tokens_usd`, or the nano-USD `pricing.*_nano_usd_per_million_tokens`
|
|
290
|
+
form — which are stored as USD per million tokens. Rates priced per image, second or character are
|
|
291
|
+
not token prices, and a tier rate beside the base one is not the base price, so neither is written. Tracked automatic
|
|
287
292
|
values may refresh; subsequent manual edits are retained. Legacy values without provenance are conservatively
|
|
288
293
|
treated as manual overrides. Unmodeled extension fields survive enrichment too.
|
|
289
294
|
- **Exact identities:** automatic lookup can map a bare, case-sensitive ID (no `/`) to a unique Gateway
|
|
@@ -320,7 +325,9 @@ Import still skips already configured accounts.
|
|
|
320
325
|
written once rather than once per provider: it gets the active provider when it can accept it, or the single
|
|
321
326
|
provider in the run that it can (Claude Code takes anthropic endpoints only, so an openai active provider does not
|
|
322
327
|
starve it). With several candidates and the active provider not among them the choice is yours, so the app keeps
|
|
323
|
-
pointing where it already does: syncing is not switching.
|
|
328
|
+
pointing where it already does: syncing is not switching. The same holds for an app that keeps a pointer of its
|
|
329
|
+
own — only the store's active provider may move it, so `sync -p other` writes that provider's block and models and
|
|
330
|
+
leaves the app's current model alone (`use` and `add` are what switch). A provider whose endpoint cannot be reached is reported
|
|
324
331
|
and skipped, the rest still sync, and the run exits non-zero.
|
|
325
332
|
- **Automatic lookup is not an automatic push:** enrichment updates saved metadata during add/quick/discover/import/refresh
|
|
326
333
|
and never writes agent configs by itself. `sync` is the push, and it refreshes first: it fetches the provider's model
|
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,7 +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
|
-
|
|
194
|
+
同一份列表里供应商为自家模型声明的上下文/输出上限与价格优先于目录的估算,只给出裸 id 的模型仍按原逻辑补全。
|
|
195
195
|
转售商列表噪音大,过滤规则持久化在供应商上,每次 `discover` 自动复用:
|
|
196
196
|
|
|
197
197
|
```bash
|
|
@@ -263,7 +263,11 @@ asw sync --provider myproxy,backup-proxy
|
|
|
263
263
|
|
|
264
264
|
- **按字段合并:** 用户手动值优先,其次端点自己声明的值,然后 models.dev,最后 Gateway 补缺。
|
|
265
265
|
端点在 `/v1/models` 中为自家模型声明了 `context_window_tokens`/`maximum_output_tokens`(或
|
|
266
|
-
`context_length`/`max_output_tokens`
|
|
266
|
+
`context_length`/`max_output_tokens` 写法)时,以它为准而不是目录对同一模型的估算;自家发布的
|
|
267
|
+
价格(`input_cost_per_million_tokens_usd`、`output_cost_per_million_tokens_usd`、
|
|
268
|
+
`cached_input_cost_per_million_tokens_usd`、`cache_write_cost_per_million_tokens_usd`,或 `pricing` 下的
|
|
269
|
+
`*_nano_usd_per_million_tokens` 纳美元写法;一律按每百万 token 美元存储)同理。按图片/秒/字符计费的不是 token 价格,
|
|
270
|
+
基础价之外的档位价也不是基础价,两者都不写。已记录来源的自动值可以刷新,
|
|
267
271
|
手动修改后会保留;旧版本中没有来源记录的已有值保守视为手动值,不强制覆盖。未知扩展字段也保留。
|
|
268
272
|
- **精确匹配:** 不含 `/` 的裸 ID 可按大小写精确匹配唯一的 Gateway `creator/model` 身份,
|
|
269
273
|
前提是 models.dev 没有身份线索或线索一致。任一目录中的创建者身份有歧义时拒绝自动匹配;
|
package/dist/commands.js
CHANGED
|
@@ -203,7 +203,7 @@ export async function cmdAdd(opts) {
|
|
|
203
203
|
const manualIds = (answers.models ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
204
204
|
let modelIds = manualIds;
|
|
205
205
|
// Limits the endpoint declares for its own models, kept from the discovery pass.
|
|
206
|
-
let
|
|
206
|
+
let endpointFacts = new Map();
|
|
207
207
|
const modelFilter = parseFilterOpts(opts, existing?.modelFilter);
|
|
208
208
|
const shouldDiscover = opts.discover || manualIds.length === 0;
|
|
209
209
|
if (shouldDiscover) {
|
|
@@ -213,7 +213,7 @@ export async function cmdAdd(opts) {
|
|
|
213
213
|
const discover = () => withProgress(t("add.discovering", { url: baseUrl }), (progress) => discoverProviderCatalog({ baseUrl, apiKey: answers.apiKey, protocol }, {
|
|
214
214
|
onPage: ({ page, count }) => progress.update(t("progress.discoverPage", { url: baseUrl, page, count })),
|
|
215
215
|
}));
|
|
216
|
-
let discovered = { ids: [],
|
|
216
|
+
let discovered = { ids: [], facts: new Map() };
|
|
217
217
|
for (let attempt = 0;; attempt++) {
|
|
218
218
|
try {
|
|
219
219
|
discovered = await discover();
|
|
@@ -233,7 +233,7 @@ export async function cmdAdd(opts) {
|
|
|
233
233
|
if (discovered.ids.length)
|
|
234
234
|
out(t("add.providerLists", { count: discovered.ids.length }));
|
|
235
235
|
modelIds = [...new Set([...manualIds, ...discovered.ids])];
|
|
236
|
-
|
|
236
|
+
endpointFacts = discovered.facts;
|
|
237
237
|
}
|
|
238
238
|
const retained = [opts.defaultModel === undefined ? existing?.defaultModel : undefined, opts.smallModel === undefined ? existing?.smallModel : undefined].filter((id) => !!id);
|
|
239
239
|
modelIds = [...new Set([...modelIds, ...retained])];
|
|
@@ -245,7 +245,7 @@ export async function cmdAdd(opts) {
|
|
|
245
245
|
fail(t("add.atLeastOne"));
|
|
246
246
|
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog());
|
|
247
247
|
const hint = opts.modelsDev ?? existing?.modelsDevId ?? guessProviderHint(catalog, answers.baseUrl);
|
|
248
|
-
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 }, { endpointFacts }));
|
|
249
249
|
const matched = models.filter((m) => m.contextWindow !== undefined).length;
|
|
250
250
|
const defaultModel = opts.defaultModel ?? existing?.defaultModel ?? modelIds[0];
|
|
251
251
|
if (!modelIds.includes(defaultModel))
|
|
@@ -292,7 +292,7 @@ async function createProvider(opts) {
|
|
|
292
292
|
const hint = opts.modelsDev ?? opts.existing?.modelsDevId ?? guessProviderHint(catalog, baseUrl);
|
|
293
293
|
const models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, modelIds, { ...metadataOptions, modelsDevId: hint, models: opts.existing?.models }, {
|
|
294
294
|
gatewayLoader: opts.gatewayLoader,
|
|
295
|
-
|
|
295
|
+
endpointFacts: opts.endpointFacts,
|
|
296
296
|
}));
|
|
297
297
|
const matched = models.filter((m) => m.contextWindow !== undefined).length;
|
|
298
298
|
const defaultModel = opts.defaultModel ?? opts.existing?.defaultModel ?? modelIds[0];
|
|
@@ -426,7 +426,7 @@ export async function cmdQuickAdd(opts) {
|
|
|
426
426
|
gatewayModels: opts.gatewayModels,
|
|
427
427
|
metadataMode: opts.metadataMode,
|
|
428
428
|
gatewayLoader,
|
|
429
|
-
|
|
429
|
+
endpointFacts: discovered.facts,
|
|
430
430
|
});
|
|
431
431
|
createdIds.push(provider.id);
|
|
432
432
|
note(`\n${t("add.next", { id: provider.id })}`);
|
|
@@ -747,7 +747,7 @@ async function refreshProviderModels(store, provider, save = true) {
|
|
|
747
747
|
const added = ids.filter((m) => !known.includes(m));
|
|
748
748
|
const removed = known.filter((m) => !retained.has(m));
|
|
749
749
|
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog());
|
|
750
|
-
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) }, { endpointFacts: listed.facts }));
|
|
751
751
|
// Reported by the caller, where it reads after the table or the per-app results instead of
|
|
752
752
|
// scrolling away above them.
|
|
753
753
|
const missing = [...new Set(pinned)].filter((model) => !ids.includes(model));
|
|
@@ -801,9 +801,10 @@ function resolveProviderIds(store, opts, fallback) {
|
|
|
801
801
|
return ids;
|
|
802
802
|
}
|
|
803
803
|
/**
|
|
804
|
-
* Apps that name a single provider are written once (see `RunTargetsOptions.oneSlot`);
|
|
805
|
-
* the
|
|
806
|
-
* on the active provider and
|
|
804
|
+
* Apps that name a single provider are written once (see `RunTargetsOptions.oneSlot`); every other
|
|
805
|
+
* app only lets the store's active provider move its "current" pointer (see `ApplyOptions.keepSelection`),
|
|
806
|
+
* so a multi-provider sync finishes on the active provider and a run that does not include it leaves
|
|
807
|
+
* the pointer where the app already had it.
|
|
807
808
|
*/
|
|
808
809
|
function activeLast(store, ids) {
|
|
809
810
|
const active = store.active;
|
|
@@ -895,7 +896,7 @@ async function syncProviders(opts, sourceStore) {
|
|
|
895
896
|
}
|
|
896
897
|
}
|
|
897
898
|
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
|
+
const outcome = await runWithOptionalDryRun("apply", provider, opts.apps, opts.dryRun, oneSlot, { keepDefault: opts.keepDefault, keepSelection: provider.id !== store.active });
|
|
899
900
|
if (outcome.failed?.length)
|
|
900
901
|
failed.push(id);
|
|
901
902
|
if (refresh?.missing.length) {
|
|
@@ -1381,7 +1382,7 @@ export async function cmdImport(opts) {
|
|
|
1381
1382
|
continue;
|
|
1382
1383
|
}
|
|
1383
1384
|
let ids = [...c.models];
|
|
1384
|
-
let
|
|
1385
|
+
let endpointFacts = new Map();
|
|
1385
1386
|
if (ids.length === 0) {
|
|
1386
1387
|
const discover = () => withProgress(t("import.discovering", { id: c.id }), (progress) => discoverProviderCatalog({ baseUrl: c.baseUrl, apiKey: apiKey, protocol: c.protocol }, {
|
|
1387
1388
|
onPage: ({ page, count }) => progress.update(t("progress.discoverPage", { url: c.baseUrl, page, count })),
|
|
@@ -1408,14 +1409,14 @@ export async function cmdImport(opts) {
|
|
|
1408
1409
|
continue;
|
|
1409
1410
|
}
|
|
1410
1411
|
ids = listed.ids;
|
|
1411
|
-
|
|
1412
|
+
endpointFacts = listed.facts;
|
|
1412
1413
|
ids = applyModelFilter(ids, undefined, []).kept;
|
|
1413
1414
|
}
|
|
1414
1415
|
if (ids.length === 0)
|
|
1415
1416
|
fail(t("import.noModelsImport", { id: c.id }));
|
|
1416
1417
|
const id = availableProviderId(c.id, store.providers);
|
|
1417
1418
|
const hint = guessProviderHint(catalog, c.baseUrl);
|
|
1418
|
-
const models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, ids, { ...metadataOptions, modelsDevId: hint }, { gatewayLoader,
|
|
1419
|
+
const models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, ids, { ...metadataOptions, modelsDevId: hint }, { gatewayLoader, endpointFacts }));
|
|
1419
1420
|
const defaultModel = c.defaultModel && ids.includes(c.defaultModel) ? c.defaultModel : ids[0];
|
|
1420
1421
|
const existed = store.providers[id] !== undefined;
|
|
1421
1422
|
store.providers[id] = {
|
package/dist/discover.js
CHANGED
|
@@ -14,7 +14,7 @@ function modelListUrl(baseUrl) {
|
|
|
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
16
|
/**
|
|
17
|
-
* Spellings endpoints use for the same
|
|
17
|
+
* Spellings endpoints use for the same fact, in priority order: the first
|
|
18
18
|
* declared key wins when a row carries several. Providers do not agree on
|
|
19
19
|
* names — an OpenAI-compatible gateway publishes context_window_tokens /
|
|
20
20
|
* maximum_output_tokens, OpenRouter publishes context_length /
|
|
@@ -23,6 +23,35 @@ export const MAX_MODEL_PAGES = 100;
|
|
|
23
23
|
*/
|
|
24
24
|
const contextWindowKeys = ["context_window_tokens", "context_length", "max_context_length"];
|
|
25
25
|
const maxOutputKeys = ["maximum_output_tokens", "max_output_tokens", "max_completion_tokens"];
|
|
26
|
+
const costSpellings = {
|
|
27
|
+
input: [["input_cost_per_million_tokens_usd", 1], ["pricing.input_nano_usd_per_million_tokens", 1e9]],
|
|
28
|
+
output: [["output_cost_per_million_tokens_usd", 1], ["pricing.output_nano_usd_per_million_tokens", 1e9]],
|
|
29
|
+
cacheRead: [["cached_input_cost_per_million_tokens_usd", 1], ["pricing.cached_input_nano_usd_per_million_tokens", 1e9]],
|
|
30
|
+
cacheWrite: [
|
|
31
|
+
["cache_write_cost_per_million_tokens_usd", 1],
|
|
32
|
+
["pricing.cache_write_nano_usd_per_million_tokens", 1e9],
|
|
33
|
+
["pricing.cache_creation_input_nano_usd_per_million_tokens", 1e9],
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
/** Read a dotted path, stopping at the first value that is not an object. */
|
|
37
|
+
function readPath(row, path) {
|
|
38
|
+
let node = row;
|
|
39
|
+
for (const key of path.split(".")) {
|
|
40
|
+
if (!node || typeof node !== "object")
|
|
41
|
+
return undefined;
|
|
42
|
+
node = node[key];
|
|
43
|
+
}
|
|
44
|
+
return node;
|
|
45
|
+
}
|
|
46
|
+
/** A price keeps its fraction — a route charging 0.15 per million says so — and 0 is a real price. */
|
|
47
|
+
function declaredCost(row, spellings) {
|
|
48
|
+
for (const [path, divisor] of spellings) {
|
|
49
|
+
const value = readPath(row, path);
|
|
50
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0)
|
|
51
|
+
return value / divisor;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
26
55
|
function declaredLimit(row, keys) {
|
|
27
56
|
for (const key of keys) {
|
|
28
57
|
const value = row[key];
|
|
@@ -36,13 +65,28 @@ function declaredLimit(row, keys) {
|
|
|
36
65
|
}
|
|
37
66
|
return undefined;
|
|
38
67
|
}
|
|
39
|
-
/**
|
|
40
|
-
export function
|
|
68
|
+
/** What a model row declares about its own route, or undefined when it describes nothing but its id. */
|
|
69
|
+
export function parseEndpointFacts(row) {
|
|
41
70
|
const contextWindow = declaredLimit(row, contextWindowKeys);
|
|
42
71
|
const maxOutput = declaredLimit(row, maxOutputKeys);
|
|
43
|
-
|
|
72
|
+
const input = declaredCost(row, costSpellings.input);
|
|
73
|
+
const output = declaredCost(row, costSpellings.output);
|
|
74
|
+
const cacheRead = declaredCost(row, costSpellings.cacheRead);
|
|
75
|
+
const cacheWrite = declaredCost(row, costSpellings.cacheWrite);
|
|
76
|
+
const costs = {
|
|
77
|
+
...(input !== undefined ? { input } : {}),
|
|
78
|
+
...(output !== undefined ? { output } : {}),
|
|
79
|
+
...(cacheRead !== undefined ? { cacheRead } : {}),
|
|
80
|
+
...(cacheWrite !== undefined ? { cacheWrite } : {}),
|
|
81
|
+
};
|
|
82
|
+
const hasCost = Object.keys(costs).length > 0;
|
|
83
|
+
if (contextWindow === undefined && maxOutput === undefined && !hasCost)
|
|
44
84
|
return undefined;
|
|
45
|
-
return {
|
|
85
|
+
return {
|
|
86
|
+
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
87
|
+
...(maxOutput !== undefined ? { maxOutput } : {}),
|
|
88
|
+
...(hasCost ? { cost: costs } : {}),
|
|
89
|
+
};
|
|
46
90
|
}
|
|
47
91
|
function parseModelPage(body) {
|
|
48
92
|
if (!body || typeof body !== "object")
|
|
@@ -54,7 +98,7 @@ function parseModelPage(body) {
|
|
|
54
98
|
if (page.has_more !== undefined && typeof page.has_more !== "boolean")
|
|
55
99
|
throw new Error("invalid model-list pagination flag");
|
|
56
100
|
const ids = [];
|
|
57
|
-
const
|
|
101
|
+
const facts = new Map();
|
|
58
102
|
for (const row of rows) {
|
|
59
103
|
if (!row || typeof row !== "object")
|
|
60
104
|
continue;
|
|
@@ -62,19 +106,19 @@ function parseModelPage(body) {
|
|
|
62
106
|
if (typeof id !== "string" || id.length === 0)
|
|
63
107
|
continue;
|
|
64
108
|
ids.push(id);
|
|
65
|
-
const declared =
|
|
109
|
+
const declared = parseEndpointFacts(row);
|
|
66
110
|
if (declared)
|
|
67
|
-
|
|
111
|
+
facts.set(id, declared);
|
|
68
112
|
}
|
|
69
|
-
return { ids,
|
|
113
|
+
return { ids, facts, hasMore: page.has_more === true, lastId: typeof page.last_id === "string" ? page.last_id : undefined };
|
|
70
114
|
}
|
|
71
115
|
/**
|
|
72
|
-
* List the provider's own /v1/models catalog: model ids plus the limits
|
|
73
|
-
* declares for itself. Plenty of resellers expose bare ids — those
|
|
74
|
-
* entry in `
|
|
75
|
-
* (enrichModels). An endpoint that
|
|
76
|
-
* own route, so
|
|
77
|
-
* whatever a third party lists for the same model.
|
|
116
|
+
* List the provider's own /v1/models catalog: model ids plus the limits and
|
|
117
|
+
* prices a row declares for itself. Plenty of resellers expose bare ids — those
|
|
118
|
+
* ids carry no entry in `facts` and metadata is filled from models.dev
|
|
119
|
+
* afterwards (enrichModels). An endpoint that declares its own numbers is
|
|
120
|
+
* authoritative for its own route, so they outrank the catalogs instead of
|
|
121
|
+
* being replaced by whatever a third party lists for the same model.
|
|
78
122
|
*/
|
|
79
123
|
export async function discoverProviderCatalog(opts, hooks = {}) {
|
|
80
124
|
const url = modelListUrl(opts.baseUrl);
|
|
@@ -82,7 +126,7 @@ export async function discoverProviderCatalog(opts, hooks = {}) {
|
|
|
82
126
|
? { "x-api-key": opts.apiKey, "anthropic-version": "2023-06-01" }
|
|
83
127
|
: { authorization: `Bearer ${opts.apiKey}` };
|
|
84
128
|
const ids = new Set();
|
|
85
|
-
const
|
|
129
|
+
const facts = new Map();
|
|
86
130
|
const cursors = new Set();
|
|
87
131
|
const initialCursor = url.searchParams.get("after_id");
|
|
88
132
|
if (initialCursor)
|
|
@@ -99,11 +143,11 @@ export async function discoverProviderCatalog(opts, hooks = {}) {
|
|
|
99
143
|
ids.add(id);
|
|
100
144
|
// A repeated id keeps what an earlier page declared: merge per key so a
|
|
101
145
|
// later partial row cannot drop the other limit.
|
|
102
|
-
for (const [id, declared] of parsed.
|
|
146
|
+
for (const [id, declared] of parsed.facts) {
|
|
103
147
|
if (!ids.has(id))
|
|
104
148
|
continue;
|
|
105
|
-
const earlier =
|
|
106
|
-
|
|
149
|
+
const earlier = facts.get(id);
|
|
150
|
+
facts.set(id, earlier ? { ...earlier, ...declared } : declared);
|
|
107
151
|
}
|
|
108
152
|
hooks.onPage?.({ page, count: ids.size });
|
|
109
153
|
if (!parsed.hasMore)
|
|
@@ -117,7 +161,7 @@ export async function discoverProviderCatalog(opts, hooks = {}) {
|
|
|
117
161
|
}
|
|
118
162
|
if (ids.size === 0)
|
|
119
163
|
throw new Error("provider returned no models");
|
|
120
|
-
return { ids: [...ids].sort(),
|
|
164
|
+
return { ids: [...ids].sort(), facts };
|
|
121
165
|
}
|
|
122
166
|
/** The listed ids alone, for callers that only need the model list. */
|
|
123
167
|
export async function discoverProviderModels(opts, hooks = {}) {
|
package/dist/metadata.js
CHANGED
|
@@ -70,13 +70,13 @@ 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
|
-
/**
|
|
74
|
-
const endpointFields = ["contextWindow", "maxOutput"];
|
|
73
|
+
/** What a wire is trusted for: the limits it enforces and the prices it charges, not capabilities. */
|
|
74
|
+
const endpointFields = ["contextWindow", "maxOutput", "cost.input", "cost.output", "cost.cacheRead", "cost.cacheWrite"];
|
|
75
75
|
/**
|
|
76
76
|
* Which source owns a field it proposes. An endpoint that declares its own
|
|
77
|
-
* limits outranks both catalogs — models.dev describes the model,
|
|
78
|
-
* Gateway describes *that* gateway, and neither is the route serving
|
|
79
|
-
* request. models.dev outranks Gateway, which only fills what it is given.
|
|
77
|
+
* limits and prices outranks both catalogs — models.dev describes the model,
|
|
78
|
+
* the public Gateway describes *that* gateway, and neither is the route serving
|
|
79
|
+
* the request. models.dev outranks Gateway, which only fills what it is given.
|
|
80
80
|
*/
|
|
81
81
|
const sourceRank = { endpoint: 3, "models.dev": 2, "ai-gateway": 1 };
|
|
82
82
|
const limits = ["maxInput", "maxOutput"];
|
|
@@ -387,8 +387,8 @@ export async function enrichProviderModels(catalog, ids, settings, options = {})
|
|
|
387
387
|
}
|
|
388
388
|
// The endpoint's own numbers go in first so the catalogs can only fill
|
|
389
389
|
// what it left unsaid; a catalog that disagrees is reported as a conflict
|
|
390
|
-
// instead of overwriting the
|
|
391
|
-
const declared = options.
|
|
390
|
+
// instead of overwriting the numbers the route actually enforces.
|
|
391
|
+
const declared = options.endpointFacts?.get(id);
|
|
392
392
|
if (declared)
|
|
393
393
|
mergeSource(spec, { id, ...declared }, "endpoint", id, now);
|
|
394
394
|
let primary;
|
package/dist/targets/codex.js
CHANGED
|
@@ -29,7 +29,7 @@ export const codex = transactionalTarget({
|
|
|
29
29
|
return [codexConfigFile(), codexAuthFile()];
|
|
30
30
|
},
|
|
31
31
|
detect: () => fs.existsSync(codexDir()),
|
|
32
|
-
async apply(provider) {
|
|
32
|
+
async apply(provider, options = {}) {
|
|
33
33
|
const configFile = codexConfigFile();
|
|
34
34
|
const authFile = codexAuthFile();
|
|
35
35
|
const notes = [];
|
|
@@ -42,16 +42,18 @@ export const codex = transactionalTarget({
|
|
|
42
42
|
if (config.model_providers !== undefined && !isJsonObject(config.model_providers)) {
|
|
43
43
|
throw new Error(`${configFile}: expected model_providers to be a table`);
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
if (!options.keepSelection) {
|
|
46
|
+
config.model_provider = provider.id;
|
|
47
|
+
config.model = provider.defaultModel;
|
|
48
|
+
const model = provider.models.find((m) => m.id === provider.defaultModel);
|
|
49
|
+
if (provider.reasoningEffort && model?.reasoning !== false) {
|
|
50
|
+
config.model_reasoning_effort = provider.reasoningEffort;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// The effort belongs to the active model: switching providers must not
|
|
54
|
+
// inherit the previous provider's level (prune treats it as owned too).
|
|
55
|
+
delete config.model_reasoning_effort;
|
|
56
|
+
}
|
|
55
57
|
}
|
|
56
58
|
// Codex only speaks the OpenAI Responses API (wire_api = "chat" was removed Feb 2026).
|
|
57
59
|
// requires_openai_auth = true makes Codex take the key from auth.json instead of an env var.
|
package/dist/targets/dsh.js
CHANGED
|
@@ -68,7 +68,7 @@ export const dsh = transactionalTarget({
|
|
|
68
68
|
return [dshSettingsFile(), dshCredentialsFile()];
|
|
69
69
|
},
|
|
70
70
|
detect: () => fs.existsSync(dshDir()),
|
|
71
|
-
async apply(provider) {
|
|
71
|
+
async apply(provider, options = {}) {
|
|
72
72
|
const notes = [];
|
|
73
73
|
const file = dshSettingsFile();
|
|
74
74
|
const doc = parseSettings(file);
|
|
@@ -117,7 +117,9 @@ export const dsh = transactionalTarget({
|
|
|
117
117
|
notes.push("dropped modelOverrides (llm-pi-ai refuses it beside an explicit models list)");
|
|
118
118
|
}
|
|
119
119
|
// The picker writes this section whole; a partial write would keep a stale reasoningEffort.
|
|
120
|
-
|
|
120
|
+
if (!options.keepSelection) {
|
|
121
|
+
doc.set("agent-default-model", doc.createNode({ provider: provider.id, model: provider.defaultModel }));
|
|
122
|
+
}
|
|
121
123
|
const referenced = credentialReferences(doc, provider.id);
|
|
122
124
|
const oldRef = typeof prev?.apiKeyEnv === "string" && isManagedCredentialRef(prev.apiKeyEnv, provider.id) && !referenced.has(prev.apiKeyEnv)
|
|
123
125
|
? prev.apiKeyEnv : undefined;
|
package/dist/targets/hermes.js
CHANGED
|
@@ -54,7 +54,7 @@ export const hermes = transactionalTarget({
|
|
|
54
54
|
protocols: ["openai", "anthropic"],
|
|
55
55
|
configPaths: [hermesConfigFile(), hermesEnvFile()],
|
|
56
56
|
detect: () => fs.existsSync(hermesHome()),
|
|
57
|
-
async apply(provider) {
|
|
57
|
+
async apply(provider, options = {}) {
|
|
58
58
|
const configFile = hermesConfigFile();
|
|
59
59
|
const envFile = hermesEnvFile();
|
|
60
60
|
const notes = [];
|
|
@@ -101,8 +101,10 @@ export const hermes = transactionalTarget({
|
|
|
101
101
|
else
|
|
102
102
|
doc.deleteIn([...modelAt, "context_length"]);
|
|
103
103
|
}
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
if (!options.keepSelection) {
|
|
105
|
+
doc.setIn(["model", "provider"], provider.id);
|
|
106
|
+
doc.setIn(["model", "default"], provider.defaultModel);
|
|
107
|
+
}
|
|
106
108
|
const envText = readTextIfExists(envFile) ?? "";
|
|
107
109
|
const assignments = envAssignments(envFile, envText);
|
|
108
110
|
const referenced = credentialReferences(doc, provider.id);
|
package/dist/targets/opencode.js
CHANGED
|
@@ -19,7 +19,7 @@ export const opencode = transactionalTarget({
|
|
|
19
19
|
},
|
|
20
20
|
detect: () => opencodeConfigFiles().some((file) => fs.existsSync(file)) ||
|
|
21
21
|
opencodeConfigDirs().some((dir) => fs.existsSync(dir)),
|
|
22
|
-
async apply(provider) {
|
|
22
|
+
async apply(provider, options = {}) {
|
|
23
23
|
const configFile = opencodePrimaryConfigFile();
|
|
24
24
|
const notes = [];
|
|
25
25
|
const existed = readTextIfExists(configFile) !== undefined;
|
|
@@ -94,14 +94,16 @@ export const opencode = transactionalTarget({
|
|
|
94
94
|
models,
|
|
95
95
|
};
|
|
96
96
|
config.provider = providerMap;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
97
|
+
if (!options.keepSelection) {
|
|
98
|
+
config.model = `${provider.id}/${provider.defaultModel}`;
|
|
99
|
+
if (provider.smallModel) {
|
|
100
|
+
config.small_model = `${provider.id}/${provider.smallModel}`;
|
|
101
|
+
}
|
|
102
|
+
else if (typeof config.small_model === "string" && config.small_model.startsWith(`${provider.id}/`)) {
|
|
103
|
+
// This provider's own previous small model is obsolete; another provider's
|
|
104
|
+
// entry stays untouched, exactly as prune scopes its cleanup.
|
|
105
|
+
delete config.small_model;
|
|
106
|
+
}
|
|
105
107
|
}
|
|
106
108
|
const backup = backupFile(configFile);
|
|
107
109
|
if (backup)
|
package/dist/targets/pistyle.js
CHANGED
|
@@ -106,7 +106,7 @@ export function piStyleTarget(opts) {
|
|
|
106
106
|
};
|
|
107
107
|
modelsConfig.providers = providers;
|
|
108
108
|
const modelsText = editJsoncObject(modelsDocument, modelsConfig);
|
|
109
|
-
const settingsText = options.keepDefault ? undefined : editJsoncObject(settingsDocument, {
|
|
109
|
+
const settingsText = options.keepDefault || options.keepSelection ? undefined : editJsoncObject(settingsDocument, {
|
|
110
110
|
...settingsDocument.value,
|
|
111
111
|
defaultProvider: provider.id,
|
|
112
112
|
defaultModel: provider.defaultModel,
|
|
@@ -79,7 +79,7 @@ export const workbuddy = transactionalTarget({
|
|
|
79
79
|
return [path.join(configDir(), "models.json"), path.join(configDir(), "settings.json")];
|
|
80
80
|
},
|
|
81
81
|
detect: () => fs.existsSync(configDir()),
|
|
82
|
-
async apply(provider) {
|
|
82
|
+
async apply(provider, options = {}) {
|
|
83
83
|
const dir = configDir();
|
|
84
84
|
const modelsFile = path.join(dir, "models.json");
|
|
85
85
|
const settingsFile = path.join(dir, "settings.json");
|
|
@@ -127,7 +127,8 @@ export const workbuddy = transactionalTarget({
|
|
|
127
127
|
const availableModels = [...new Set([
|
|
128
128
|
...existingAvailable.filter((id) => !removedIds.has(id) || keptIds.has(id)), ...ourIds,
|
|
129
129
|
])];
|
|
130
|
-
|
|
130
|
+
if (!options.keepSelection)
|
|
131
|
+
settings.model = provider.defaultModel;
|
|
131
132
|
const modelsBackup = backupFile(modelsFile);
|
|
132
133
|
if (modelsBackup)
|
|
133
134
|
notes.push(`backup: ${modelsBackup}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentsw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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": {
|