agentsw 0.7.2 → 0.9.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 +82 -4
- package/README.zh-CN.md +67 -3
- package/dist/commands.js +60 -18
- package/dist/fsutil.js +14 -1
- package/dist/gateway.js +201 -0
- package/dist/i18n.js +26 -6
- package/dist/index.js +26 -1
- package/dist/jsonc.js +4 -5
- package/dist/menu.js +62 -4
- package/dist/metadata.js +400 -0
- package/dist/modelsdev.js +28 -8
- package/dist/targets/codex.js +15 -4
- package/dist/yaml.js +28 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ ok dsh ~/.dsh/settings.yaml, ~/.dsh/.credentials.yaml
|
|
|
45
45
|
keys it does not model — `authHeader`, `headers`, `compat`, `discovery` — per-model extras
|
|
46
46
|
like `thinkingLevelMap`, and YAML comments all survive a re-sync.
|
|
47
47
|
- **Model metadata, not just an id list.** Reseller `/v1/models` endpoints return bare ids.
|
|
48
|
-
agentsw enriches them from [models.dev](https://models.dev) and writes what each app
|
|
48
|
+
agentsw enriches them from [models.dev](https://models.dev), automatically fills meaningful gaps from AI Gateway when needed, and writes what each app
|
|
49
49
|
actually reads: `thinkingLevelMap` for pi/prime, `limit`/`attachment` for opencode,
|
|
50
50
|
`contextWindow`/`maxTokens` for omp, `reasoningEfforts`/`input` for dsh.
|
|
51
51
|
- **Adopts what you already have** — including [cc-switch](https://github.com/farion1231/cc-switch).
|
|
@@ -121,6 +121,12 @@ dsh yes openai+anthropic vfing · glm-5.3-flash ~/.ds
|
|
|
121
121
|
An agent that is not installed is skipped, not guessed at. `--apps codex,omp` narrows a run;
|
|
122
122
|
`--apps` with an uninstalled agent forces it.
|
|
123
123
|
|
|
124
|
+
The interactive menu asks the same question before writing: a multi-select where the first row
|
|
125
|
+
is **all detected apps**, detected agents are listed first and undetected ones are marked. The
|
|
126
|
+
last selection is remembered, so a menu `use`/`sync` keeps rewriting only the agents you picked.
|
|
127
|
+
An error in any config file (invalid JSON/YAML/TOML) is reported with the file and position, never
|
|
128
|
+
with the offending line, and dry-run keeps that detail instead of hiding it.
|
|
129
|
+
|
|
124
130
|
## Import what you already configured
|
|
125
131
|
|
|
126
132
|
`import` reads every agent's config **and cc-switch's SQLite store** (read-only), resolves
|
|
@@ -146,8 +152,8 @@ use `--all` explicitly in scripts. Model discovery may update the local metadata
|
|
|
146
152
|
## Model discovery filters
|
|
147
153
|
|
|
148
154
|
`--discover` lists model ids from the provider's `/v1/models`, then enriches each id from
|
|
149
|
-
models.dev
|
|
150
|
-
every `discover`:
|
|
155
|
+
models.dev first, with automatic AI Gateway supplementation when needed. Reseller lists are noisy,
|
|
156
|
+
so filters are persisted per provider and re-applied on every `discover`:
|
|
151
157
|
|
|
152
158
|
```bash
|
|
153
159
|
# snapshot duplicates (gpt-5.2-latest, glm-4.7-250414, ...) are DROPPED by default
|
|
@@ -160,6 +166,77 @@ asw discover myproxy --no-filter # clear it
|
|
|
160
166
|
|
|
161
167
|
Explicit `--models` entries and the default model are never filtered out.
|
|
162
168
|
|
|
169
|
+
## Automatic model metadata
|
|
170
|
+
|
|
171
|
+
**No extra flags are needed.** New providers and providers without a saved mode use `auto`:
|
|
172
|
+
models.dev is checked first, then the public
|
|
173
|
+
[AI Gateway model catalog](https://vercel.com/docs/ai-gateway/models-and-providers#dynamic-model-discovery)
|
|
174
|
+
is consulted only for missing core parameters (context window, maximum output, reasoning or image input),
|
|
175
|
+
to refresh unchanged Gateway-supplied fields that models.dev has not replaced, or to verify an identity
|
|
176
|
+
conflict in previously tracked automatic values. Missing names, prices, optional input limits or
|
|
177
|
+
reasoning-effort lists alone do not trigger a Gateway lookup.
|
|
178
|
+
agentsw directly fetches `https://ai-gateway.vercel.sh/v1/models` without provider credentials or an AI SDK dependency.
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# Refresh only saved models using the saved mode (auto if unset)
|
|
182
|
+
# No model discovery or agent config writes
|
|
183
|
+
asw refresh --provider myproxy
|
|
184
|
+
|
|
185
|
+
# Inspect effective mode, sources, conflicts and reference pricing (no API key)
|
|
186
|
+
asw models --provider myproxy --metadata
|
|
187
|
+
|
|
188
|
+
# Custom or ambiguous names need explicit aliases; wire model IDs stay unchanged
|
|
189
|
+
asw refresh --provider myproxy --metadata-mode auto \
|
|
190
|
+
--gateway-models '{"my-model":"openai/gpt-5.4"}'
|
|
191
|
+
|
|
192
|
+
# Always consult Gateway, even when core parameters are complete
|
|
193
|
+
asw refresh --provider myproxy --metadata-mode on
|
|
194
|
+
|
|
195
|
+
# Stop future Gateway fetches; retain saved model parameters
|
|
196
|
+
asw refresh --provider myproxy --metadata-mode off
|
|
197
|
+
|
|
198
|
+
# Return an explicitly disabled provider to automatic mode
|
|
199
|
+
asw refresh --provider myproxy --metadata-mode auto
|
|
200
|
+
|
|
201
|
+
# Preview and apply separately; sync does not query catalogs
|
|
202
|
+
asw sync --provider myproxy --dry-run
|
|
203
|
+
asw sync --provider myproxy
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The menu's **Configure and refresh model metadata** action offers **Automatic (recommended)**,
|
|
207
|
+
**Always supplement**, and **Disabled**, with the provider's current mode selected. Onboarding has no extra prompts.
|
|
208
|
+
`add`, `quick`, `discover`, `import`, and `refresh` accept `--metadata-mode <auto|on|off>`.
|
|
209
|
+
The existing `--gateway-metadata` and `--no-gateway-metadata` flags remain explicit `on` and `off` choices.
|
|
210
|
+
Omitted options preserve saved settings: an existing `false` remains off until you select `auto` or `on`.
|
|
211
|
+
Invalid modes and conflicting mode/boolean flags are rejected before fetching or saving.
|
|
212
|
+
Without `--provider`, `refresh` applies to all providers, including any supplied mode.
|
|
213
|
+
`--gateway-models` replaces the whole alias map (`{}` clears it); aliases do not change the saved mode or override `off`.
|
|
214
|
+
Import still skips already configured accounts.
|
|
215
|
+
|
|
216
|
+
- **Field-level priority:** manual values > models.dev > Gateway gap filling. Tracked automatic values may
|
|
217
|
+
refresh; subsequent manual edits are retained. Legacy values without provenance are conservatively treated
|
|
218
|
+
as manual overrides. Unmodeled extension fields survive enrichment too.
|
|
219
|
+
- **Exact identities:** automatic lookup can map a bare, case-sensitive ID (no `/`) to a unique Gateway
|
|
220
|
+
`creator/model` identity when models.dev evidence is absent or agrees. Ambiguous creator evidence in either
|
|
221
|
+
catalog is rejected; custom or ambiguous names need explicit aliases. Qualified IDs never have their prefixes
|
|
222
|
+
stripped, and matching never guesses from substrings, case folding or versions. Auto/on mode also uses conservative
|
|
223
|
+
exact models.dev matching; off retains the legacy models.dev lookup. Unmatched models remain usable.
|
|
224
|
+
Alias changes clear unchanged automatic values tied to confirmed superseded identities, never manual edits.
|
|
225
|
+
- **No routing changes:** metadata never adds available models or changes IDs, protocols, URLs, defaults or
|
|
226
|
+
accounts. Gateway capabilities are reference specs, not proof of reseller support. `supported_specifications`
|
|
227
|
+
is not a URL version.
|
|
228
|
+
- **Reference-only prices:** Gateway prices are converted to USD per million tokens and stored separately in
|
|
229
|
+
`metadata.gateway.referenceCost`, not effective provider `cost` or runtime agent pricing. Variable/tiered
|
|
230
|
+
pricing is flagged separately.
|
|
231
|
+
Auto does not fetch solely to refresh this reference snapshot: check `metadata.gateway.fetchedAt`, or use
|
|
232
|
+
`refresh --provider <id> --metadata-mode on` to request fresh reference prices.
|
|
233
|
+
- **Auditable and optional:** `metadata.fields` records origins, value snapshots and timestamps;
|
|
234
|
+
`metadata.conflicts` records disagreements with retained values. This audit stays in agentsw, never runtime agent configs.
|
|
235
|
+
Gateway has a separate 24-hour cache; failures use stale cached data or skip supplementation.
|
|
236
|
+
- **Automatic lookup is not automatic sync:** enrichment updates saved metadata during add/quick/discover/import/refresh.
|
|
237
|
+
It does not add an automatic push to agents. Sync behavior is unchanged: ordinary `sync` only writes saved settings
|
|
238
|
+
and fetches neither model lists nor metadata catalogs.
|
|
239
|
+
|
|
163
240
|
## Commands
|
|
164
241
|
|
|
165
242
|
| Command | What it does |
|
|
@@ -175,7 +252,8 @@ Explicit `--models` entries and the default model are never filtered out.
|
|
|
175
252
|
| `sync` | re-apply the active provider (after an agent update, say) |
|
|
176
253
|
| `discover <id> [--sync]` | refresh the model list + metadata from `/v1/models` |
|
|
177
254
|
| `models [query]` | search the models.dev catalog |
|
|
178
|
-
| `refresh` |
|
|
255
|
+
| `refresh [--provider <id>]` | refresh saved metadata and optionally configure Gateway; keep the model list |
|
|
256
|
+
| `models --provider <id> --metadata` | inspect field sources, conflicts and reference pricing as JSON |
|
|
179
257
|
| `prune <id>` / `remove <id> [--prune]` | remove from app configs / from the store |
|
|
180
258
|
| `remove <id> --apps omp` | delete only the selected app's entry; supports `--dry-run` |
|
|
181
259
|
| `apps` / `install <app>` / `upgrade` | agent version manager |
|
package/README.zh-CN.md
CHANGED
|
@@ -44,7 +44,7 @@ ok dsh ~/.dsh/settings.yaml, ~/.dsh/.credentials.yaml
|
|
|
44
44
|
(`authHeader`、`headers`、`compat`、`discovery`)、`thinkingLevelMap` 之类的模型级字段、
|
|
45
45
|
以及 YAML 注释,都会在重新同步后原样保留。
|
|
46
46
|
- **写的是元数据,不只是模型 id。** 转售商 `/v1/models` 只返回裸 id;agentsw 从
|
|
47
|
-
[models.dev](https://models.dev)
|
|
47
|
+
[models.dev](https://models.dev) 优先补全,并在有实际缺口时自动查询 AI Gateway,按各应用真正读取的字段下发:pi/prime 的
|
|
48
48
|
`thinkingLevelMap`、opencode 的 `limit`/`attachment`、omp 的 `contextWindow`/`maxTokens`、
|
|
49
49
|
dsh 的 `reasoningEfforts`/`input`。
|
|
50
50
|
- **接管你已有的配置——包括 [cc-switch](https://github.com/farion1231/cc-switch)。**
|
|
@@ -143,7 +143,7 @@ imported zhipu-glm · anthropic · https://open.bigmodel.cn/api/anthropic · 1 m
|
|
|
143
143
|
|
|
144
144
|
## 模型发现过滤
|
|
145
145
|
|
|
146
|
-
`--discover` 从供应商 `/v1/models` 拉取模型 id
|
|
146
|
+
`--discover` 从供应商 `/v1/models` 拉取模型 id,优先从 models.dev 补全元数据,必要时自动查询 AI Gateway。
|
|
147
147
|
转售商列表噪音大,过滤规则持久化在供应商上,每次 `discover` 自动复用:
|
|
148
148
|
|
|
149
149
|
```bash
|
|
@@ -157,6 +157,69 @@ asw discover myproxy --no-filter # 清除规则
|
|
|
157
157
|
|
|
158
158
|
手动 `--models` 指定的 id 和默认模型永不被过滤。
|
|
159
159
|
|
|
160
|
+
## 自动补全模型元数据
|
|
161
|
+
|
|
162
|
+
**默认无需额外参数。** 新供应商和未保存模式的供应商使用 `auto`:先查询 models.dev,
|
|
163
|
+
仅在核心参数(上下文窗口、最大输出、推理或图片输入能力)仍缺失、已有 Gateway 自动字段
|
|
164
|
+
未被手动修改或 models.dev 替代而需刷新,或需核实旧自动值的身份冲突时,才查询公开的
|
|
165
|
+
[AI Gateway 模型目录](https://vercel.com/docs/ai-gateway/models-and-providers#dynamic-model-discovery)。
|
|
166
|
+
仅缺名称、价格、可选输入上限或推理等级列表,不会触发 Gateway 查询。
|
|
167
|
+
直接请求 `https://ai-gateway.vercel.sh/v1/models`,不携带供应商密钥,也不安装 AI SDK。
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# 按保存的模式刷新已有模型参数(未设置时为 auto)
|
|
171
|
+
# 不发现新模型、不写智能体配置
|
|
172
|
+
asw refresh --provider myproxy
|
|
173
|
+
|
|
174
|
+
# 查看生效模式、字段来源、冲突和参考价格;JSON 不含 API key
|
|
175
|
+
asw models --provider myproxy --metadata
|
|
176
|
+
|
|
177
|
+
# 自定义或有歧义的名称需显式映射;实际调用的模型 ID 不变
|
|
178
|
+
asw refresh --provider myproxy --metadata-mode auto \
|
|
179
|
+
--gateway-models '{"my-model":"openai/gpt-5.4"}'
|
|
180
|
+
|
|
181
|
+
# 即使核心参数完整,也始终查询 Gateway
|
|
182
|
+
asw refresh --provider myproxy --metadata-mode on
|
|
183
|
+
|
|
184
|
+
# 停止后续 Gateway 获取;保留已保存的参数
|
|
185
|
+
asw refresh --provider myproxy --metadata-mode off
|
|
186
|
+
|
|
187
|
+
# 将显式关闭的供应商恢复为自动模式
|
|
188
|
+
asw refresh --provider myproxy --metadata-mode auto
|
|
189
|
+
|
|
190
|
+
# 单独预览、同步;sync 不查询目录
|
|
191
|
+
asw sync --provider myproxy --dry-run
|
|
192
|
+
asw sync --provider myproxy
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
交互菜单的“设置并刷新模型参数补充源”提供“自动按需(推荐)”“始终补充”“关闭”,
|
|
196
|
+
默认选中该供应商当前模式;添加供应商时不增加提问。
|
|
197
|
+
`add`、`quick`、`discover`、`import`、`refresh` 均支持 `--metadata-mode <auto|on|off>`。
|
|
198
|
+
原有 `--gateway-metadata` / `--no-gateway-metadata` 仍分别表示显式 `on` / `off`。
|
|
199
|
+
省略参数会保留已保存的设置:已有 `false` 仍保持关闭,直到主动选择 `auto` 或 `on`。
|
|
200
|
+
无效模式或相互冲突的模式/布尔开关,会在查询或保存之前报错。
|
|
201
|
+
`refresh` 不传 `--provider` 会作用于全部供应商,包括更新传入的模式。
|
|
202
|
+
`--gateway-models` 替换整个映射,传 `{}` 清空;映射不会更改保存的模式,也不会覆盖 `off`。
|
|
203
|
+
导入仍跳过已经配置的账号。
|
|
204
|
+
|
|
205
|
+
- **按字段合并:** 用户手动值优先,其次 models.dev,最后 Gateway 补缺。已记录来源的自动值可以刷新,
|
|
206
|
+
手动修改后会保留;旧版本中没有来源记录的已有值保守视为手动值,不强制覆盖。未知扩展字段也保留。
|
|
207
|
+
- **精确匹配:** 不含 `/` 的裸 ID 可按大小写精确匹配唯一的 Gateway `creator/model` 身份,
|
|
208
|
+
前提是 models.dev 没有身份线索或线索一致。任一目录中的创建者身份有歧义时拒绝自动匹配;
|
|
209
|
+
自定义或有歧义的名称仍需显式映射。带命名空间的 ID 不会剥离前缀,不按子串、大小写折叠或版本相似度猜测。
|
|
210
|
+
`auto` / `on` 模式下 models.dev 也采用保守的精确匹配;`off` 保留原有 models.dev 查询行为。
|
|
211
|
+
匹配失败不妨碍模型继续使用。修改/删除映射时,只清理确认已失效身份的自动值,保留手动修改。
|
|
212
|
+
- **不改变调用语义:** 目录不会给供应商增加模型,不改模型 ID、协议、URL、默认模型或账户;
|
|
213
|
+
Gateway 能力是参考规格,不保证转售商实际实现。`supported_specifications` 不是 URL 版本。
|
|
214
|
+
- **价格仅供参考:** Gateway 价格转换为美元/百万 token,单独保存在 `metadata.gateway.referenceCost`,
|
|
215
|
+
不写入供应商的有效 `cost`,也不下发为智能体计费价格;分档/供应商差异另有标记。
|
|
216
|
+
自动模式不会仅为刷新参考价格而查询:可查看 `metadata.gateway.fetchedAt` 判断快照时间,
|
|
217
|
+
需要重新查询参考价格时使用 `refresh --provider <id> --metadata-mode on`。
|
|
218
|
+
- **可追溯、可降级:** `metadata.fields` 记录来源、原值快照和时间,`metadata.conflicts` 记录保留值与冲突。
|
|
219
|
+
审计信息仅保存在 agentsw,不写入智能体运行配置。Gateway 目录独立缓存 24 小时;请求失败可使用旧缓存,无缓存时只跳过补充。
|
|
220
|
+
- **自动查询不等于自动同步:** 添加、快速添加、发现、导入、刷新时补全保存的元数据,并不新增自动推送。
|
|
221
|
+
同步行为不变:普通 `sync` 仅写入已保存的设置,既不获取模型列表,也不请求元数据目录。
|
|
222
|
+
|
|
160
223
|
## 命令
|
|
161
224
|
|
|
162
225
|
| 命令 | 作用 |
|
|
@@ -172,7 +235,8 @@ asw discover myproxy --no-filter # 清除规则
|
|
|
172
235
|
| `sync` | 重新应用当前供应商(比如某个智能体升级之后) |
|
|
173
236
|
| `discover <id> [--sync]` | 从 `/v1/models` 刷新模型列表与元数据 |
|
|
174
237
|
| `models [query]` | 搜索 models.dev 目录 |
|
|
175
|
-
| `refresh` |
|
|
238
|
+
| `refresh [--provider <id>]` | 刷新已有模型参数,可设置 Gateway 补充源,不改变模型列表 |
|
|
239
|
+
| `models --provider <id> --metadata` | 以 JSON 查看字段来源、冲突和参考价格 |
|
|
176
240
|
| `prune <id>` / `remove <id> [--prune]` | 从各应用配置中清除 / 从存储中删除 |
|
|
177
241
|
| `remove <id> --apps omp` | 仅删除指定智能体内的条目,支持 `--dry-run` |
|
|
178
242
|
| `apps` / `install <app>` / `upgrade` | 智能体版本管理 |
|
package/dist/commands.js
CHANGED
|
@@ -7,14 +7,21 @@ import pc from "picocolors";
|
|
|
7
7
|
import prompts from "prompts";
|
|
8
8
|
import { loadStore, saveStore, getProvider, configFile } from "./store.js";
|
|
9
9
|
import { scanCandidates, normalizeUrl, findMatchingProvider } from "./import.js";
|
|
10
|
-
import {
|
|
10
|
+
import { loadCatalog, searchCatalog } from "./modelsdev.js";
|
|
11
|
+
import { enrichProviderModels, getMetadataMode, resolveMetadataOptions } from "./metadata.js";
|
|
12
|
+
import { loadGatewayCatalog } from "./gateway.js";
|
|
11
13
|
import { resolveTargets, supportsProtocol, targets } from "./targets/index.js";
|
|
12
14
|
import { discoverProviderModels, probeProtocols } from "./discover.js";
|
|
13
15
|
import { appCommand, appPackages, installedVersion, isNewer, latestVersion, normalizeAppVersion, runShell } from "./apps.js";
|
|
14
|
-
import { drainPendingWrites, readTextIfExists, setDryRun } from "./fsutil.js";
|
|
16
|
+
import { SafeConfigError, drainPendingWrites, readTextIfExists, setDryRun } from "./fsutil.js";
|
|
15
17
|
import { applyModelFilter } from "./filter.js";
|
|
16
18
|
import { availableProviderId, providerIdFromBaseUrl, providerNameFromBaseUrl } from "./slug.js";
|
|
17
19
|
import { t } from "./i18n.js";
|
|
20
|
+
/** Share successes and failures without fetching until a model actually needs the supplement. */
|
|
21
|
+
function sharedGatewayLoader(refresh = false) {
|
|
22
|
+
let pending;
|
|
23
|
+
return () => pending ??= loadGatewayCatalog({ refresh }).then((catalog) => catalog ?? null);
|
|
24
|
+
}
|
|
18
25
|
function fail(message) {
|
|
19
26
|
process.stderr.write(pc.red(`error: ${message}\n`));
|
|
20
27
|
process.exit(1);
|
|
@@ -58,16 +65,16 @@ function modelRows(models) {
|
|
|
58
65
|
fmtTokens(m.contextWindow),
|
|
59
66
|
fmtTokens(m.maxInput),
|
|
60
67
|
fmtTokens(m.maxOutput),
|
|
61
|
-
m.reasoning ? (m.reasoningEfforts?.join("/")
|
|
68
|
+
m.reasoning === undefined ? "-" : m.reasoning ? (m.reasoningEfforts?.length ? m.reasoningEfforts.join("/") : "yes") : "no",
|
|
62
69
|
m.cost ? `$${m.cost.input ?? "?"}/$${m.cost.output ?? "?"}` : "-",
|
|
63
70
|
]);
|
|
64
71
|
}
|
|
65
72
|
const MODEL_HEADER = ["MODEL", "CTX", "IN", "OUT", "REASONING", "$IN/$OUT per M"];
|
|
66
|
-
/** Print a dim hint when some models have no
|
|
73
|
+
/** Print a dim hint when some models have no catalog metadata. */
|
|
67
74
|
function printUncatalogedHint(models) {
|
|
68
75
|
const bare = models.filter((m) => m.contextWindow === undefined && !m.cost);
|
|
69
76
|
if (bare.length > 0) {
|
|
70
|
-
console.log(pc.dim(`${bare.length} model(s) have no
|
|
77
|
+
console.log(pc.dim(`${bare.length} model(s) have no catalog metadata (shown as "-")`));
|
|
71
78
|
}
|
|
72
79
|
}
|
|
73
80
|
/** Guess the models.dev provider whose API host matches the configured baseUrl. */
|
|
@@ -113,6 +120,7 @@ function reportDropped(dropped) {
|
|
|
113
120
|
}
|
|
114
121
|
export async function cmdAdd(opts) {
|
|
115
122
|
const store = loadStore();
|
|
123
|
+
resolveMetadataOptions(opts);
|
|
116
124
|
const interactive = process.stdin.isTTY && !opts.yes;
|
|
117
125
|
let answers = {};
|
|
118
126
|
if (interactive) {
|
|
@@ -192,6 +200,7 @@ export async function cmdAdd(opts) {
|
|
|
192
200
|
const existing = explicitId ? store.providers[explicitId] : findMatchingProvider(Object.values(store.providers), { protocol, baseUrl, apiKey: answers.apiKey });
|
|
193
201
|
const id = explicitId ?? existing?.id ?? availableProviderId(providerIdFromBaseUrl(baseUrl, protocol), store.providers);
|
|
194
202
|
const openaiApi = protocol === "openai" ? wire ?? existing?.openaiApi : undefined;
|
|
203
|
+
const metadataOptions = resolveMetadataOptions(opts, existing);
|
|
195
204
|
const manualIds = (answers.models ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
196
205
|
let modelIds = manualIds;
|
|
197
206
|
const modelFilter = parseFilterOpts(opts, existing?.modelFilter);
|
|
@@ -215,7 +224,7 @@ export async function cmdAdd(opts) {
|
|
|
215
224
|
fail(t("add.atLeastOne"));
|
|
216
225
|
const catalog = await loadCatalog();
|
|
217
226
|
const hint = opts.modelsDev ?? existing?.modelsDevId ?? guessProviderHint(catalog, answers.baseUrl);
|
|
218
|
-
const models =
|
|
227
|
+
const models = await enrichProviderModels(catalog, modelIds, { ...metadataOptions, modelsDevId: hint, models: existing?.models });
|
|
219
228
|
const matched = models.filter((m) => m.contextWindow !== undefined).length;
|
|
220
229
|
const defaultModel = opts.defaultModel ?? existing?.defaultModel ?? modelIds[0];
|
|
221
230
|
if (!modelIds.includes(defaultModel))
|
|
@@ -235,6 +244,7 @@ export async function cmdAdd(opts) {
|
|
|
235
244
|
smallModel: opts.smallModel ?? existing?.smallModel,
|
|
236
245
|
reasoningEffort: opts.reasoningEffort ?? existing?.reasoningEffort,
|
|
237
246
|
modelsDevId: hint,
|
|
247
|
+
...metadataOptions,
|
|
238
248
|
modelFilter,
|
|
239
249
|
};
|
|
240
250
|
const existed = store.providers[provider.id] !== undefined;
|
|
@@ -252,9 +262,10 @@ export async function cmdAdd(opts) {
|
|
|
252
262
|
/** Shared logic to create and save a single provider from discovered models. */
|
|
253
263
|
async function createProvider(opts) {
|
|
254
264
|
const { store, id, name, protocol, openaiApi, baseUrl, apiKey, modelIds, modelFilter } = opts;
|
|
265
|
+
const metadataOptions = resolveMetadataOptions(opts, opts.existing);
|
|
255
266
|
const catalog = await loadCatalog();
|
|
256
267
|
const hint = opts.modelsDev ?? opts.existing?.modelsDevId ?? guessProviderHint(catalog, baseUrl);
|
|
257
|
-
const models =
|
|
268
|
+
const models = await enrichProviderModels(catalog, modelIds, { ...metadataOptions, modelsDevId: hint, models: opts.existing?.models }, { gatewayLoader: opts.gatewayLoader });
|
|
258
269
|
const matched = models.filter((m) => m.contextWindow !== undefined).length;
|
|
259
270
|
const defaultModel = opts.defaultModel ?? opts.existing?.defaultModel ?? modelIds[0];
|
|
260
271
|
if (!modelIds.includes(defaultModel))
|
|
@@ -275,6 +286,7 @@ async function createProvider(opts) {
|
|
|
275
286
|
smallModel,
|
|
276
287
|
reasoningEffort: opts.reasoningEffort ?? opts.existing?.reasoningEffort,
|
|
277
288
|
modelsDevId: hint,
|
|
289
|
+
...metadataOptions,
|
|
278
290
|
modelFilter,
|
|
279
291
|
};
|
|
280
292
|
const existed = store.providers[provider.id] !== undefined;
|
|
@@ -295,6 +307,7 @@ async function createProvider(opts) {
|
|
|
295
307
|
*/
|
|
296
308
|
export async function cmdQuickAdd(opts) {
|
|
297
309
|
const store = loadStore();
|
|
310
|
+
resolveMetadataOptions(opts);
|
|
298
311
|
const interactive = process.stdin.isTTY && !opts.yes;
|
|
299
312
|
let baseUrl;
|
|
300
313
|
let apiKey;
|
|
@@ -323,6 +336,7 @@ export async function cmdQuickAdd(opts) {
|
|
|
323
336
|
fail(t("add.openaiApiInvalid"));
|
|
324
337
|
const multi = protocols.length > 1;
|
|
325
338
|
const createdIds = [];
|
|
339
|
+
const gatewayLoader = sharedGatewayLoader();
|
|
326
340
|
for (const protocol of protocols) {
|
|
327
341
|
const explicitId = opts.id === undefined ? undefined : `${opts.id}${multi ? `-${protocol}` : ""}`;
|
|
328
342
|
const existing = explicitId === undefined ? findMatchingProvider(Object.values(store.providers), { protocol, baseUrl, apiKey }) : store.providers[explicitId];
|
|
@@ -357,6 +371,10 @@ export async function cmdQuickAdd(opts) {
|
|
|
357
371
|
existing,
|
|
358
372
|
openaiApi: opts.openaiApi,
|
|
359
373
|
modelsDev: opts.modelsDev,
|
|
374
|
+
gatewayMetadata: opts.gatewayMetadata,
|
|
375
|
+
gatewayModels: opts.gatewayModels,
|
|
376
|
+
metadataMode: opts.metadataMode,
|
|
377
|
+
gatewayLoader,
|
|
360
378
|
});
|
|
361
379
|
createdIds.push(provider.id);
|
|
362
380
|
console.log(pc.dim(`\n${t("add.next", { id: provider.id })}`));
|
|
@@ -425,7 +443,8 @@ async function runTargets(op, provider, appsFilter, redactErrors = false) {
|
|
|
425
443
|
results.push(await target[op](provider));
|
|
426
444
|
}
|
|
427
445
|
catch (err) {
|
|
428
|
-
|
|
446
|
+
const safe = redactErrors && !(err instanceof SafeConfigError);
|
|
447
|
+
results.push({ app: target.id, changed: [], notes: [], skipped: pc.red(`failed: ${safe ? "configuration could not be previewed safely" : err.message}`) });
|
|
429
448
|
process.exitCode = 1;
|
|
430
449
|
}
|
|
431
450
|
}
|
|
@@ -605,9 +624,21 @@ export function cmdStatus() {
|
|
|
605
624
|
console.log(table(rows, ["APP", t("table.found"), t("table.protocols"), t("table.current"), t("table.config")]));
|
|
606
625
|
}
|
|
607
626
|
export async function cmdModels(query, opts) {
|
|
627
|
+
if (opts.metadata && !opts.provider)
|
|
628
|
+
fail("--metadata requires --provider");
|
|
608
629
|
if (opts.provider) {
|
|
609
630
|
const store = loadStore();
|
|
610
631
|
const provider = getProvider(store, opts.provider);
|
|
632
|
+
if (opts.metadata) {
|
|
633
|
+
console.log(JSON.stringify({
|
|
634
|
+
provider: provider.id,
|
|
635
|
+
gatewayMetadata: provider.gatewayMetadata ?? "auto",
|
|
636
|
+
metadataMode: getMetadataMode(provider),
|
|
637
|
+
gatewayModelAliases: provider.gatewayModelAliases ?? {},
|
|
638
|
+
models: provider.models.map(({ id, metadata }) => ({ id, metadata })),
|
|
639
|
+
}, null, 2));
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
611
642
|
console.log(`${pc.bold(provider.id)} (${provider.protocol}) · ${provider.baseUrl}`);
|
|
612
643
|
console.log(table(modelRows(provider.models), MODEL_HEADER));
|
|
613
644
|
printUncatalogedHint(provider.models);
|
|
@@ -630,20 +661,26 @@ export async function cmdModels(query, opts) {
|
|
|
630
661
|
});
|
|
631
662
|
console.log(table(rows, ["PROVIDER", ...MODEL_HEADER]));
|
|
632
663
|
}
|
|
633
|
-
export async function cmdRefreshMeta() {
|
|
664
|
+
export async function cmdRefreshMeta(opts = {}) {
|
|
634
665
|
const store = loadStore();
|
|
666
|
+
const providers = opts.provider ? [getProvider(store, opts.provider)] : Object.values(store.providers);
|
|
667
|
+
resolveMetadataOptions(opts);
|
|
668
|
+
for (const provider of providers)
|
|
669
|
+
Object.assign(provider, resolveMetadataOptions(opts, provider));
|
|
635
670
|
const catalog = await loadCatalog({ refresh: true });
|
|
636
|
-
|
|
637
|
-
fail("models.dev catalog unavailable");
|
|
671
|
+
const gatewayLoader = sharedGatewayLoader(true);
|
|
638
672
|
let updated = 0;
|
|
639
|
-
for (const provider of
|
|
673
|
+
for (const provider of providers) {
|
|
640
674
|
const before = JSON.stringify(provider.models);
|
|
641
|
-
provider.models =
|
|
675
|
+
provider.models = await enrichProviderModels(catalog, provider.models.map((m) => m.id), {
|
|
676
|
+
...provider,
|
|
677
|
+
modelsDevId: provider.modelsDevId ?? guessProviderHint(catalog, provider.baseUrl),
|
|
678
|
+
}, { gatewayLoader });
|
|
642
679
|
if (JSON.stringify(provider.models) !== before)
|
|
643
680
|
updated++;
|
|
644
681
|
}
|
|
645
682
|
saveStore(store);
|
|
646
|
-
console.log(pc.green(`
|
|
683
|
+
console.log(pc.green(`checked model metadata (${updated} provider(s) changed)`));
|
|
647
684
|
if (updated > 0)
|
|
648
685
|
console.log(pc.dim("run `agentsw sync` to push updated metadata into app configs"));
|
|
649
686
|
}
|
|
@@ -651,6 +688,7 @@ export async function cmdDiscover(id, opts) {
|
|
|
651
688
|
resolveTargets(opts.apps);
|
|
652
689
|
const store = loadStore();
|
|
653
690
|
const provider = getProvider(store, id);
|
|
691
|
+
Object.assign(provider, resolveMetadataOptions(opts, provider));
|
|
654
692
|
// flags override and re-persist the filter; --no-filter clears it
|
|
655
693
|
const flagFilter = parseFilterOpts(opts, provider.modelFilter);
|
|
656
694
|
if (opts.filter === false)
|
|
@@ -659,17 +697,18 @@ export async function cmdDiscover(id, opts) {
|
|
|
659
697
|
provider.modelFilter = flagFilter;
|
|
660
698
|
process.stderr.write(pc.dim(`discovering models from ${provider.baseUrl} ...\n`));
|
|
661
699
|
const listed = await discoverProviderModels(provider);
|
|
662
|
-
const
|
|
700
|
+
const pinned = [provider.defaultModel, provider.smallModel].filter((model) => !!model);
|
|
701
|
+
const outcome = applyModelFilter(listed, provider.modelFilter, pinned);
|
|
663
702
|
reportDropped(outcome.dropped);
|
|
664
703
|
const ids = outcome.kept;
|
|
665
704
|
const known = provider.models.map((m) => m.id);
|
|
666
705
|
const added = ids.filter((m) => !known.includes(m));
|
|
667
706
|
const gone = known.filter((m) => !ids.includes(m));
|
|
668
707
|
const catalog = await loadCatalog();
|
|
669
|
-
|
|
708
|
+
const retainedIds = [...new Set([...ids, ...pinned])];
|
|
709
|
+
provider.models = await enrichProviderModels(catalog, retainedIds, { ...provider, modelsDevId: provider.modelsDevId ?? guessProviderHint(catalog, provider.baseUrl) });
|
|
670
710
|
if (!ids.includes(provider.defaultModel)) {
|
|
671
711
|
console.log(pc.yellow(`default model ${provider.defaultModel} no longer listed; keeping it anyway`));
|
|
672
|
-
provider.models.push({ id: provider.defaultModel });
|
|
673
712
|
}
|
|
674
713
|
saveStore(store);
|
|
675
714
|
console.log(`${pc.bold(id)}: ${ids.length} models (${pc.green(`+${added.length}`)} / ${pc.red(`-${gone.length}`)})` +
|
|
@@ -820,6 +859,7 @@ export async function cmdUpgrade(ids) {
|
|
|
820
859
|
}
|
|
821
860
|
/** Collect custom providers, dedupe by endpoint and credentials, and import what is new. */
|
|
822
861
|
export async function cmdImport(opts) {
|
|
862
|
+
const metadataOptions = resolveMetadataOptions(opts);
|
|
823
863
|
const rows = scanCandidates();
|
|
824
864
|
const fresh = rows.filter((r) => !r.configured);
|
|
825
865
|
for (const r of rows) {
|
|
@@ -864,6 +904,7 @@ export async function cmdImport(opts) {
|
|
|
864
904
|
}
|
|
865
905
|
const store = loadStore();
|
|
866
906
|
const catalog = await loadCatalog();
|
|
907
|
+
const gatewayLoader = sharedGatewayLoader();
|
|
867
908
|
const imported = [];
|
|
868
909
|
for (const c of chosen) {
|
|
869
910
|
let apiKey = c.apiKey;
|
|
@@ -901,7 +942,7 @@ export async function cmdImport(opts) {
|
|
|
901
942
|
fail(t("import.noModelsImport", { id: c.id }));
|
|
902
943
|
const id = availableProviderId(c.id, store.providers);
|
|
903
944
|
const hint = guessProviderHint(catalog, c.baseUrl);
|
|
904
|
-
const models =
|
|
945
|
+
const models = await enrichProviderModels(catalog, ids, { ...metadataOptions, modelsDevId: hint }, { gatewayLoader });
|
|
905
946
|
const defaultModel = c.defaultModel && ids.includes(c.defaultModel) ? c.defaultModel : ids[0];
|
|
906
947
|
const existed = store.providers[id] !== undefined;
|
|
907
948
|
store.providers[id] = {
|
|
@@ -914,6 +955,7 @@ export async function cmdImport(opts) {
|
|
|
914
955
|
models,
|
|
915
956
|
defaultModel,
|
|
916
957
|
modelsDevId: hint,
|
|
958
|
+
...metadataOptions,
|
|
917
959
|
};
|
|
918
960
|
if (!store.active)
|
|
919
961
|
store.active = id;
|
package/dist/fsutil.js
CHANGED
|
@@ -3,6 +3,13 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
+
/**
|
|
7
|
+
* An error whose message is guaranteed to carry no file content. Dry-run redacts
|
|
8
|
+
* unknown errors wholesale (parser messages can embed credentials), so config
|
|
9
|
+
* parsers must throw this to stay actionable there.
|
|
10
|
+
*/
|
|
11
|
+
export class SafeConfigError extends Error {
|
|
12
|
+
}
|
|
6
13
|
// AGENTSW_HOME is an explicit portable/test override. Native Windows otherwise
|
|
7
14
|
// uses USERPROFILE/os.homedir(), while Unix follows HOME as expected.
|
|
8
15
|
export const home = process.env.AGENTSW_HOME?.trim() ||
|
|
@@ -88,7 +95,13 @@ export function readJsonIfExists(file) {
|
|
|
88
95
|
const text = readTextIfExists(file);
|
|
89
96
|
if (text === undefined)
|
|
90
97
|
return undefined;
|
|
91
|
-
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(text);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Node quotes surrounding input in the message, which may hold credentials.
|
|
103
|
+
throw new SafeConfigError(`${file}: invalid JSON configuration`);
|
|
104
|
+
}
|
|
92
105
|
}
|
|
93
106
|
/** Compatibility preview switch. Each target captures its value when its scoped work starts. */
|
|
94
107
|
let dryRun = false;
|