agentsw 0.10.2 → 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/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 = rows.flatMap((row) => {
51
+ const ids = [];
52
+ const limits = new Map();
53
+ for (const row of rows) {
26
54
  if (!row || typeof row !== "object")
27
- return [];
55
+ continue;
28
56
  const id = row.id;
29
- return typeof id === "string" && id.length > 0 ? [id] : [];
30
- });
31
- return { ids, hasMore: page.has_more === true, lastId: typeof page.last_id === "string" ? page.last_id : undefined };
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 model ids from the provider's own /v1/models endpoint.
35
- * Resellers expose the id list here but no metadata (context, thinking, modalities) —
36
- * metadata is filled from models.dev afterwards (enrichModels).
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 discoverProviderModels(opts, hooks = {}) {
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
@@ -14,10 +14,10 @@ const messages = {
14
14
  "cmd.prune": { en: "remove a provider's entries from app configs (keeps it in agentsw)", "zh-CN": "从应用配置移除供应商条目(仍保留在 agentsw)" },
15
15
  "cmd.rename": { en: "rename a provider and migrate config references with backups", "zh-CN": "重命名供应商并迁移配置引用(自动备份)" },
16
16
  "cmd.use": { en: "set the active provider and write it into app configs", "zh-CN": "设为当前供应商并写入应用配置" },
17
- "cmd.sync": { en: "re-apply the active provider (or --provider) to app configs", "zh-CN": "将当前供应商(或 --provider)重新应用到应用配置" },
17
+ "cmd.sync": { en: "refresh a provider's model list and re-apply it to app configs (--no-refresh writes the saved list)", "zh-CN": "刷新供应商模型列表并重新应用到应用配置(--no-refresh 只写入已保存的列表)" },
18
18
  "cmd.status": { en: "show detected apps and what they currently point at", "zh-CN": "显示检测到的应用及当前供应商" },
19
19
  "cmd.models": { en: "search models.dev, or show a configured provider's models", "zh-CN": "搜索 models.dev 或显示已配置供应商的模型" },
20
- "cmd.discover": { en: "refresh a provider's model list and configured metadata sources", "zh-CN": "获取供应商模型列表,并通过已启用的目录补全参数" },
20
+ "cmd.discover": { en: "refresh model lists and configured metadata sources for one or more providers", "zh-CN": "刷新一个或多个供应商的模型列表,并通过已启用的目录补全参数" },
21
21
  "cmd.import": { en: "scan app configs, preview and dedupe existing custom providers, then import", "zh-CN": "扫描应用配置,预览并去重已有自定义供应商后导入" },
22
22
  "cmd.apps": { en: "check installed agents: current version vs latest", "zh-CN": "检查智能体已安装版本与最新版本" },
23
23
  "cmd.install": { en: "install an agent CLI", "zh-CN": "安装智能体 CLI" },
@@ -35,6 +35,7 @@ const messages = {
35
35
  "opt.models": { en: "comma-separated model ids", "zh-CN": "逗号分隔的模型 id" },
36
36
  "opt.defaultModel": { en: "default model (defaults to first)", "zh-CN": "默认模型(默认取第一个)" },
37
37
  "opt.smallModel": { en: "small/fast model (Claude Code haiku slot)", "zh-CN": "小型/快速模型(Claude Code haiku 槽位)" },
38
+ "opt.noSmallModel": { en: "clear the small/fast model so the default model fills that slot", "zh-CN": "清除小型/快速模型,该槽位改用默认模型" },
38
39
  "opt.reasoning": { en: "preferred reasoning effort (codex): minimal|low|medium|high", "zh-CN": "Codex 首选推理等级: minimal|low|medium|high" },
39
40
  "opt.discover": { en: "list model ids from the provider's /v1/models", "zh-CN": "从供应商 /v1/models 获取模型 id" },
40
41
  "opt.include": { en: "keep only models matching comma-separated globs", "zh-CN": "仅保留匹配逗号分隔 glob 的模型" },
@@ -47,7 +48,7 @@ const messages = {
47
48
  "opt.noGatewayMetadata": { en: "disable Gateway fetching; keep saved parameters (same as --metadata-mode off)", "zh-CN": "关闭 Gateway 获取,保留已保存参数(等同 --metadata-mode off)" },
48
49
  "opt.gatewayModels": { en: 'replace exact aliases as JSON: {"local-id":"creator/model"}; {} clears them', "zh-CN": '用 JSON 替换精确映射:{"本地模型ID":"creator/model"};{} 清空' },
49
50
  "opt.metadataDetails": { en: "show field sources, conflicts and Gateway reference pricing as JSON (requires --provider)", "zh-CN": "以 JSON 查看字段来源、冲突和 Gateway 参考价格(需 --provider)" },
50
- "opt.refreshProvider": { en: "refresh/configure only this saved provider (default: all)", "zh-CN": "仅刷新和设置该供应商(默认全部)" },
51
+ "opt.refreshProvider": { en: "refresh/configure only these saved providers, comma-separated (default: all)", "zh-CN": "仅刷新和设置这些供应商,逗号分隔(默认全部)" },
51
52
  "opt.yes": { en: "non-interactive; require all flags", "zh-CN": "非交互模式;要求提供全部参数" },
52
53
  "opt.prune": { en: "also remove entries from app configs", "zh-CN": "同时从应用配置中移除条目" },
53
54
  "opt.apps": { en: "comma-separated apps or 'all'", "zh-CN": "逗号分隔的应用或 'all'" },
@@ -56,9 +57,17 @@ const messages = {
56
57
  "opt.appsDetailed": { en: "comma-separated apps (claude,codex,omp,pi,prime,opencode,hermes,workbuddy,dsh) or 'all'", "zh-CN": "逗号分隔的应用(claude,codex,omp,pi,prime,opencode,hermes,workbuddy,dsh)或 'all'" },
57
58
  "opt.model": { en: "override default model while switching", "zh-CN": "切换时覆盖默认模型" },
58
59
  "opt.dryRun": { en: "preview config diff without writing", "zh-CN": "仅预览配置差异,不写入" },
59
- "opt.provider": { en: "sync this provider instead of the active one", "zh-CN": "同步指定供应商而非当前供应商" },
60
+ "opt.provider": { en: "sync these providers instead of the active one (comma-separated)", "zh-CN": "同步指定供应商而非当前供应商(逗号分隔)" },
61
+ "opt.syncAll": { en: "sync every configured provider", "zh-CN": "同步全部已配置的供应商" },
62
+ "opt.refreshAll": { en: "refresh every configured provider", "zh-CN": "刷新全部已配置的供应商" },
60
63
  "opt.showProvider": { en: "show models of a configured provider", "zh-CN": "显示已配置供应商的模型" },
61
64
  "opt.refresh": { en: "force-refresh models.dev cache", "zh-CN": "强制刷新 models.dev 缓存" },
65
+ "opt.noRefresh": { en: "skip the model-list refresh; write the saved list only", "zh-CN": "跳过模型列表刷新;只写入已保存的列表" },
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} 的供应商/模型;应用默认选择保持不变" },
62
71
  "opt.limit": { en: "maximum results", "zh-CN": "最大结果数" },
63
72
  "opt.syncAfter": { en: "push refreshed provider into app configs", "zh-CN": "刷新后推送到应用配置" },
64
73
  "opt.appsSync": { en: "apps to sync when --sync is set", "zh-CN": "--sync 时要同步的应用" },
@@ -80,8 +89,7 @@ const messages = {
80
89
  "menu.use": { en: "Switch provider and default model", "zh-CN": "切换供应商和默认模型" },
81
90
  "menu.status": { en: "View each agent's current configuration", "zh-CN": "查看各智能体当前配置" },
82
91
  "menu.list": { en: "View providers saved in agentsw", "zh-CN": "查看 agentsw 供应商列表" },
83
- "menu.sync": { en: "Re-sync the current provider to agents", "zh-CN": "重新同步当前供应商" },
84
- "menu.discover": { en: "Update a provider's model list", "zh-CN": "更新供应商模型列表" },
92
+ "menu.sync": { en: "Sync providers", "zh-CN": "同步供应商" },
85
93
  "menu.metadata": { en: "Configure and refresh model metadata", "zh-CN": "设置并刷新模型参数补充源" },
86
94
  "menu.metadataHelp": { en: "Keep the model list; choose automatic, always-on or disabled AI Gateway lookup, then refresh saved parameters only.", "zh-CN": "保留模型列表,选择 AI Gateway 自动按需、始终补充或关闭,仅刷新保存的参数。" },
87
95
  "menu.metadataProvider": { en: "which provider's metadata settings should be updated?", "zh-CN": "选择要设置模型参数补充源的供应商" },
@@ -128,8 +136,8 @@ const messages = {
128
136
  "menu.appsAll": { en: "all {count} detected apps", "zh-CN": "全部 {count} 个检测到的应用" },
129
137
  "menu.appNotDetected": { en: "(not detected)", "zh-CN": "(未检测到)" },
130
138
  "menu.appsHint": { en: "↑/↓ move · Space toggle · Enter confirm", "zh-CN": "↑/↓ 移动 · 空格切换 · Enter 确认" },
131
- "menu.discoverFor": { en: "which provider's model list should be updated?", "zh-CN": "选择要更新模型列表的供应商" },
132
- "menu.pushRefresh": { en: "also sync this provider to agent configs after updating its model list?", "zh-CN": "更新模型列表后,是否同时将该供应商同步到智能体配置?" },
139
+ "menu.syncFor": { en: "which providers should be synced?", "zh-CN": "选择要同步的供应商(可多选)" },
140
+ "menu.providersAll": { en: "all {count} configured providers", "zh-CN": "全部 {count} 个已配置供应商" },
133
141
  "menu.removeProvider": { en: "which provider configuration should be deleted?", "zh-CN": "选择要删除配置的供应商" },
134
142
  "menu.upgrade": { en: "upgrade all installed agents that have updates?", "zh-CN": "是否升级所有有新版本的已安装智能体?" },
135
143
  "menu.installApp": { en: "Install a new coding agent", "zh-CN": "安装新的智能体" },
@@ -205,8 +213,7 @@ const messages = {
205
213
  "menu.useHelp": { en: "Select a provider and default model, then write them to compatible agent configs.", "zh-CN": "选择供应商和默认模型,然后写入支持该协议的智能体配置。" },
206
214
  "menu.statusHelp": { en: "Show detected agents, their current providers and config file locations.", "zh-CN": "查看检测到的智能体、所用供应商及配置文件位置。" },
207
215
  "menu.listHelp": { en: "Show only the providers, protocols and default models saved in agentsw.", "zh-CN": "只查看 agentsw 已保存的供应商、协议和默认模型。" },
208
- "menu.syncHelp": { en: "Write the current agentsw provider to agent configs without fetching a model list or metadata catalogs.", "zh-CN": "将 agentsw 当前供应商写入智能体配置,不重新获取模型列表或元数据目录。" },
209
- "menu.discoverHelp": { en: "Fetch models from the provider, fill in model metadata, and optionally sync to agents.", "zh-CN": "从供应商接口重新获取模型并补全模型参数,可选择同步到智能体。" },
216
+ "menu.syncHelp": { en: "Fetch each selected provider's current model list, fill in saved parameters, then write them to the selected agent configs.", "zh-CN": "依次获取所选供应商的最新模型列表并补全保存的参数,然后写入所选智能体配置。" },
210
217
  "menu.renameHelp": { en: "Change the config ID and update references; keep custom display names.", "zh-CN": "修改配置中的标识并更新关联引用,保留自定义显示名称。" },
211
218
  "menu.removeHelp": { en: "Choose where to delete: agentsw only, one agent only, or both.", "zh-CN": "先选择删除范围:仅 agentsw、仅某个智能体,或两处一起。" },
212
219
  "menu.appsHelp": { en: "Compare installed and latest versions, then choose whether to upgrade.", "zh-CN": "对比已安装版本和最新版本,再选择是否升级。" },
@@ -241,6 +248,17 @@ const messages = {
241
248
  "use.switching": { en: "switching to {id} ({protocol}) · default model {model}", "zh-CN": "正在切换到 {id} ({protocol})· 默认模型 {model}" },
242
249
  "sync.noActive": { en: "no active provider; run agentsw use <id> first", "zh-CN": "没有当前供应商;请先运行 agentsw use <id>" },
243
250
  "sync.syncing": { en: "syncing provider {id} · default model {model}", "zh-CN": "正在同步供应商 {id} · 默认模型 {model}" },
251
+ "select.allAndProvider": { en: "--all cannot be combined with a provider selection", "zh-CN": "--all 不能与指定的供应商同时使用" },
252
+ "select.noProviders": { en: "no providers configured; run agentsw add first", "zh-CN": "尚未配置任何供应商;请先运行 agentsw add" },
253
+ "select.noneSelected": { en: "the provider selection is empty; name at least one id", "zh-CN": "未指定任何供应商,至少需要一个 id" },
254
+ "sync.refreshSkipped": { en: "skipping {id}: {message}", "zh-CN": "已跳过 {id}:{message}" },
255
+ "sync.summaryOk": { en: "synced {count} providers", "zh-CN": "已同步 {count} 个供应商" },
256
+ "sync.summaryFailed": { en: "synced {ok} of {count} providers; the rest failed", "zh-CN": "已同步 {count} 个中的 {ok} 个供应商,其余失败" },
257
+ "sync.refreshNotSaved": { en: "dry run: the refreshed model list was not saved", "zh-CN": "试运行:刷新到的模型列表未保存" },
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": "目标应用操作失败" },
244
262
  "status.config": { en: "config: {file}", "zh-CN": "配置:{file}" },
245
263
  "status.active": { en: "active provider: {id}", "zh-CN": "当前供应商:{id}" },
246
264
  "status.none": { en: "(none)", "zh-CN": "(无)" },
@@ -375,9 +393,16 @@ const messages = {
375
393
  },
376
394
  "upgrade.done": { en: "{id} -> {version}", "zh-CN": "{id} -> {version}" },
377
395
  "upgrade.failed": { en: "{id} upgrade failed: {message}", "zh-CN": "{id} 升级失败:{message}" },
378
- "discover.defaultMissing": {
379
- en: "default model {model} no longer listed; keeping it anyway",
380
- "zh-CN": "默认模型 {model} 已不在列表中;仍然保留",
396
+ "discover.summaryOk": { en: "refreshed {count} providers", "zh-CN": "已刷新 {count} 个供应商" },
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}" },
399
+ "refresh.pinnedMissing": {
400
+ en: "{models} no longer listed by the endpoint; kept as configured — pick a listed model with: agentsw use {id} -m <model>",
401
+ "zh-CN": "{models} 已不在端点的模型列表中;仍按原配置保留 —— 可用 agentsw use {id} -m <model> 换成列表中的模型",
402
+ },
403
+ "sync.pinnedMissingWritten": {
404
+ en: "{models} no longer listed by the endpoint but was written to the agents anyway; pick a listed model with: agentsw use {id} -m <model>",
405
+ "zh-CN": "{models} 已不在端点的模型列表中,但仍写入了各智能体;可用 agentsw use {id} -m <model> 换成列表中的模型",
381
406
  },
382
407
  "discover.next": {
383
408
  en: "run `agentsw sync` to push into app configs",
package/dist/index.js CHANGED
@@ -133,14 +133,19 @@ program
133
133
  .description(t("cmd.use"))
134
134
  .option("-a, --apps <apps>", t("opt.appsDetailed"))
135
135
  .option("-m, --model <id>", t("opt.model"))
136
+ .option("--small-model <id>", t("opt.smallModel"))
137
+ .option("--no-small-model", t("opt.noSmallModel"))
136
138
  .option("-n, --dry-run", t("opt.dryRun"))
137
139
  .action(cmdUse);
138
140
  program
139
141
  .command("sync")
140
142
  .description(t("cmd.sync"))
141
143
  .option("-a, --apps <apps>", t("opt.apps"))
142
- .option("-p, --provider <id>", t("opt.provider"))
144
+ .option("-p, --provider <ids>", t("opt.provider"))
145
+ .option("--all", t("opt.syncAll"))
143
146
  .option("-n, --dry-run", t("opt.dryRun"))
147
+ .option("--no-refresh", t("opt.noRefresh"))
148
+ .option("--keep-default", t("opt.keepDefault"))
144
149
  .action(cmdSync);
145
150
  program.command("status").description(t("cmd.status")).action(cmdStatus);
146
151
  program
@@ -152,10 +157,14 @@ program
152
157
  .option("-l, --limit <n>", t("opt.limit"), "30")
153
158
  .action(cmdModels);
154
159
  program
155
- .command("discover <id>")
160
+ .command("discover [ids...]")
156
161
  .description(t("cmd.discover"))
162
+ .option("-p, --provider <ids>", t("opt.refreshProvider"))
163
+ .option("--all", t("opt.refreshAll"))
157
164
  .option("-s, --sync", t("opt.syncAfter"))
158
165
  .option("-a, --apps <apps>", t("opt.appsSync"))
166
+ .option("--keep-default", t("opt.keepDefault"))
167
+ .option("-n, --dry-run", t("opt.dryRun"))
159
168
  .option("--include <globs>", t("opt.setInclude"))
160
169
  .option("--exclude <globs>", t("opt.setExclude"))
161
170
  .option("--dedup", t("opt.dedup"))
@@ -181,7 +190,8 @@ program.command("upgrade [apps...]").description(t("cmd.upgrade")).action(cmdUpg
181
190
  program
182
191
  .command("refresh")
183
192
  .description(t("cmd.refresh"))
184
- .option("-p, --provider <id>", t("opt.refreshProvider"))
193
+ .option("-p, --provider <ids>", t("opt.refreshProvider"))
194
+ .option("--all", t("opt.refreshAll"))
185
195
  .option("--metadata-mode <auto|on|off>", t("opt.metadataMode"))
186
196
  .option("--gateway-metadata", t("opt.gatewayMetadata"))
187
197
  .option("--no-gateway-metadata", t("opt.noGatewayMetadata"))
package/dist/menu.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import pc from "picocolors";
2
2
  import prompts from "prompts";
3
- import { cmdAdd, cmdQuickAdd, cmdApps, cmdInstall, cmdDiscover, cmdImport, cmdRefreshMeta, cmdList, cmdStatus, cmdSync, cmdUpgrade, cmdUse, } from "./commands.js";
3
+ import { cmdAdd, cmdQuickAdd, cmdApps, cmdInstall, cmdImport, cmdRefreshMeta, cmdList, cmdStatus, cmdSync, cmdUpgrade, cmdUse, } from "./commands.js";
4
4
  import { getLocale, setLocale, t } from "./i18n.js";
5
5
  import { getMetadataMode } from "./metadata.js";
6
6
  import { loadStore, saveStore } from "./store.js";
@@ -66,6 +66,41 @@ async function pickProvider(message) {
66
66
  const p = store.providers[id];
67
67
  return { id, defaultModel: p.defaultModel, models: p.models.map((m) => m.id) };
68
68
  }
69
+ /**
70
+ * Multi-select providers for one sync. The first row takes every configured provider and maps to
71
+ * the CLI's `--all`; toggling everything off means "back", not "all", because a sync writes files.
72
+ */
73
+ async function pickProviders(message) {
74
+ const store = loadStore();
75
+ const ids = Object.keys(store.providers);
76
+ if (ids.length === 0) {
77
+ out(pc.yellow(t("menu.noProvidersHint")));
78
+ return undefined;
79
+ }
80
+ const { providers } = await prompts({
81
+ type: "multiselect",
82
+ name: "providers",
83
+ message,
84
+ hint: t("menu.appsHint"),
85
+ instructions: false,
86
+ choices: [
87
+ { title: t("menu.providersAll", { count: ids.length }), value: "__all__" },
88
+ ...ids.map((pid) => {
89
+ const p = store.providers[pid];
90
+ return {
91
+ title: `${pid} · ${p.protocol} · ${t("menu.defaultModel")} ${p.defaultModel}${store.active === pid ? ` (${t("menu.active")})` : ""}`,
92
+ value: pid,
93
+ selected: store.active === pid,
94
+ };
95
+ }),
96
+ ],
97
+ }, cancel);
98
+ const chosen = Array.isArray(providers) ? providers : [];
99
+ if (chosen.includes("__all__"))
100
+ return { all: true, ids };
101
+ const selected = chosen.filter((value) => value !== "__all__");
102
+ return selected.length ? { all: false, ids: selected } : undefined;
103
+ }
69
104
  /**
70
105
  * Multi-select target apps, remembering the previous choice. The first row selects
71
106
  * every detected app; picking it (or toggling everything off) returns undefined,
@@ -212,7 +247,6 @@ export async function cmdMenu() {
212
247
  { title: t("menu.status"), description: t("menu.statusHelp"), value: "status" },
213
248
  { title: t("menu.list"), description: t("menu.listHelp"), value: "list" },
214
249
  { title: t("menu.sync"), description: t("menu.syncHelp"), value: "sync" },
215
- { title: t("menu.discover"), description: t("menu.discoverHelp"), value: "discover" },
216
250
  { title: t("menu.metadata"), description: t("menu.metadataHelp"), value: "metadata" },
217
251
  { title: t("menu.rename"), description: t("menu.renameHelp"), value: "rename" },
218
252
  { title: t("menu.remove"), description: t("menu.removeHelp"), value: "remove" },
@@ -271,14 +305,11 @@ export async function cmdMenu() {
271
305
  cmdList();
272
306
  }
273
307
  else if (action === "sync") {
274
- await cmdSync({ apps: await pickApps(t("menu.pickApps")) });
275
- }
276
- else if (action === "discover") {
277
- const picked = await pickProvider(t("menu.discoverFor"));
308
+ const picked = await pickProviders(t("menu.syncFor"));
278
309
  if (!picked)
279
310
  continue;
280
- const sync = await askToggle(t("menu.pushRefresh"));
281
- await cmdDiscover(picked.id, { sync, apps: sync ? await pickApps(t("menu.pickApps")) : undefined });
311
+ const apps = await pickApps(t("menu.pickApps"));
312
+ await cmdSync(picked.all ? { all: true, apps } : { provider: picked.ids.join(","), apps });
282
313
  }
283
314
  else if (action === "metadata") {
284
315
  const picked = await pickProvider(t("menu.metadataProvider"));
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
- if (!origin || origin.modelId === modelId)
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 && (source === "models.dev" || origin.source === source))) {
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);
@@ -14,6 +14,8 @@ export const claudecode = transactionalTarget({
14
14
  id: "claude",
15
15
  name: "Claude Code",
16
16
  protocols: ["anthropic"],
17
+ // The env block names one endpoint: a second provider replaces the first rather than joining it.
18
+ singleProvider: true,
17
19
  get configPaths() {
18
20
  return [claudeSettingsFile()];
19
21
  },
@@ -5,7 +5,7 @@ import { dshCredentialsFile, dshDir, dshSettingsFile } from "../app-paths.js";
5
5
  import { isManagedCredentialRef, legacyManagedCredentialRef, managedCredentialRef } from "../provider-identity.js";
6
6
  import { transactionalTarget } from "../target-transaction.js";
7
7
  import { parseYamlMapping, serializeYamlMapping } from "../yaml.js";
8
- import { apiValue, classifyApi, entryApi, mergeModels, sdkBaseUrl } from "./wire.js";
8
+ import { apiValue, classifyApi, entryApi, hasModelMetadata, mergeModels, sdkBaseUrl } from "./wire.js";
9
9
  /** Layout version of `.credentials.yaml` this build reads and writes. */
10
10
  const CREDENTIALS_VERSION = 1;
11
11
  /** Thinking levels a `reasoningEfforts` entry may offer (llm-pi-ai THINKING_LEVEL_GATE). */
@@ -29,7 +29,8 @@ function modelEntry(m) {
29
29
  ...(m.name ? { name: m.name } : {}),
30
30
  ...(m.contextWindow ? { contextWindow: m.contextWindow } : {}),
31
31
  ...(m.maxOutput ? { maxTokens: m.maxOutput } : {}),
32
- input: m.imageInput ? ["text", "image"] : ["text"],
32
+ // `input` is a guess for a spec that says nothing; see hasModelMetadata.
33
+ ...(hasModelMetadata(m) ? { input: m.imageInput ? ["text", "image"] : ["text"] } : {}),
33
34
  ...(m.reasoning === false
34
35
  ? { reasoningEfforts: false }
35
36
  : m.reasoning && m.reasoningEfforts?.length
@@ -5,9 +5,7 @@ import { ompAgentDir, ompModelsFiles } from "../app-paths.js";
5
5
  import { looksLikeEnvName } from "../slug.js";
6
6
  import { transactionalTarget } from "../target-transaction.js";
7
7
  import { parseYamlMapping, serializeYamlMapping } from "../yaml.js";
8
- import { apiValue, classifyApi, entryApi, mergeModels, sdkBaseUrl, stripConflictingOverrides } from "./wire.js";
9
- /** Per-model keys this adapter writes; one that stops being emitted is cleared, not inherited. */
10
- const OWNED_MODEL_KEYS = ["id", "name", "reasoning", "input", "contextWindow", "maxTokens", "cost"];
8
+ import { apiValue, applyDeepseekCompat, classifyApi, entryApi, mergeModels, OWNED_MODEL_METADATA_KEYS, ownedModelMetadata, sdkBaseUrl, stripConflictingOverrides, } from "./wire.js";
11
9
  /** omp has no directory override; the file set is shared with remove/rename. */
12
10
  function modelFiles() {
13
11
  const [yml, yaml] = ompModelsFiles();
@@ -43,27 +41,12 @@ export const omp = transactionalTarget({
43
41
  const anthropic = provider.protocol === "anthropic";
44
42
  const api = apiValue(provider.protocol, provider.openaiApi, prev?.api ?? entryApi(prev ?? {}));
45
43
  const baseUrl = sdkBaseUrl(provider.protocol, provider.baseUrl);
46
- const models = mergeModels(prev?.models, provider.models.map((m) => ({
47
- id: m.id,
48
- ...(m.name ? { name: m.name } : {}),
49
- ...(m.reasoning !== undefined ? { reasoning: m.reasoning } : {}),
50
- input: m.imageInput ? ["text", "image"] : ["text"],
51
- ...(m.contextWindow ? { contextWindow: m.contextWindow } : {}),
52
- ...(m.maxOutput ? { maxTokens: m.maxOutput } : {}),
53
- ...(m.cost
54
- ? {
55
- cost: {
56
- input: m.cost.input ?? 0,
57
- output: m.cost.output ?? 0,
58
- cacheRead: m.cost.cacheRead ?? 0,
59
- cacheWrite: m.cost.cacheWrite ?? 0,
60
- },
61
- }
62
- : {}),
63
- })), OWNED_MODEL_KEYS);
44
+ const models = mergeModels(prev?.models, provider.models.map(ownedModelMetadata), OWNED_MODEL_METADATA_KEYS);
64
45
  const conflicts = stripConflictingOverrides(models, api, baseUrl);
65
46
  if (conflicts.length)
66
47
  notes.push(`dropped model overrides pointing elsewhere: ${conflicts.join(", ")}`);
48
+ if (!anthropic)
49
+ applyDeepseekCompat(models, { requiresReasoningContentForToolCalls: true });
67
50
  const entry = {
68
51
  baseUrl,
69
52
  apiKey: provider.apiKey, // omp treats value as env-var name first, then literal