agentsw 0.9.1 → 0.9.2

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/commands.js CHANGED
@@ -8,7 +8,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
10
  import { loadCatalog, searchCatalog } from "./modelsdev.js";
11
- import { enrichProviderModels, getMetadataMode, resolveMetadataOptions } from "./metadata.js";
11
+ import { classifyUnresolved, enrichProviderModels, getMetadataMode, resolveMetadataOptions } from "./metadata.js";
12
12
  import { loadGatewayCatalog } from "./gateway.js";
13
13
  import { resolveTargets, supportsProtocol, targets } from "./targets/index.js";
14
14
  import { discoverProviderModels, probeProtocols } from "./discover.js";
@@ -70,11 +70,19 @@ function modelRows(models) {
70
70
  ]);
71
71
  }
72
72
  const MODEL_HEADER = ["MODEL", "CTX", "IN", "OUT", "REASONING", "$IN/$OUT per M"];
73
- /** Print a dim hint when some models have no catalog metadata. */
74
- function printUncatalogedHint(models) {
73
+ /**
74
+ * Explain why some models stayed bare: only ambiguous ids are worth the user's
75
+ * attention, so those are named separately from ids nothing can resolve.
76
+ */
77
+ function printUncatalogedHint(models, catalog, hint) {
75
78
  const bare = models.filter((m) => m.contextWindow === undefined && !m.cost);
76
- if (bare.length > 0) {
77
- console.log(pc.dim(`${bare.length} model(s) have no catalog metadata (shown as "-")`));
79
+ if (bare.length === 0)
80
+ return;
81
+ console.log(pc.dim(t("meta.gap", { count: bare.length })));
82
+ const { ambiguous, unknown, noCreatorRow } = classifyUnresolved(catalog, bare.map((m) => m.id), hint);
83
+ for (const [key, ids] of [["meta.ambiguous", ambiguous], ["meta.noCreatorRow", noCreatorRow], ["meta.unknown", unknown]]) {
84
+ if (ids.length)
85
+ console.log(pc.dim(` ${t(key, { count: ids.length, ids: ids.join(", "), id: ids[0] })}`));
78
86
  }
79
87
  }
80
88
  /** Guess the models.dev provider whose API host matches the configured baseUrl. */
@@ -197,8 +205,15 @@ export async function cmdAdd(opts) {
197
205
  fail(t("add.openaiApiInvalid"));
198
206
  const baseUrl = normalizeUrl(answers.baseUrl);
199
207
  const explicitId = answers.id || undefined;
200
- const existing = explicitId ? store.providers[explicitId] : findMatchingProvider(Object.values(store.providers), { protocol, baseUrl, apiKey: answers.apiKey });
201
- const id = explicitId ?? existing?.id ?? availableProviderId(providerIdFromBaseUrl(baseUrl, protocol), store.providers);
208
+ const byId = explicitId ? store.providers[explicitId] : undefined;
209
+ // Endpoint plus credential is the account identity, so an explicit id never
210
+ // licenses a second entry for an account that is already configured.
211
+ const sameAccount = byId ? undefined : findMatchingProvider(Object.values(store.providers), { protocol, baseUrl, apiKey: answers.apiKey });
212
+ const existing = byId ?? sameAccount;
213
+ if (byId === undefined && explicitId && sameAccount) {
214
+ console.log(pc.yellow(t("add.alreadyConfigured", { id: pc.bold(sameAccount.id) })));
215
+ }
216
+ const id = byId?.id ?? sameAccount?.id ?? explicitId ?? availableProviderId(providerIdFromBaseUrl(baseUrl, protocol), store.providers);
202
217
  const openaiApi = protocol === "openai" ? wire ?? existing?.openaiApi : undefined;
203
218
  const metadataOptions = resolveMetadataOptions(opts, existing);
204
219
  const manualIds = (answers.models ?? "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -256,7 +271,7 @@ export async function cmdAdd(opts) {
256
271
  console.log(pc.green(t("add.saved", { status: savedStatus, id: pc.bold(provider.id), protocol })));
257
272
  console.log(t("add.metadata", { matched, total: models.length }) + (hint ? t("add.providerHint", { hint }) : ""));
258
273
  console.log(table(modelRows(models), MODEL_HEADER));
259
- printUncatalogedHint(models);
274
+ printUncatalogedHint(models, catalog, hint);
260
275
  console.log(pc.dim(`\n${t("add.next", { id: provider.id })}`));
261
276
  }
262
277
  /** Shared logic to create and save a single provider from discovered models. */
@@ -297,7 +312,7 @@ async function createProvider(opts) {
297
312
  console.log(pc.green(t("add.saved", { status: savedStatus, id: pc.bold(provider.id), protocol })));
298
313
  console.log(t("add.metadata", { matched, total: models.length }) + (hint ? t("add.providerHint", { hint }) : ""));
299
314
  console.log(table(modelRows(models), MODEL_HEADER));
300
- printUncatalogedHint(models);
315
+ printUncatalogedHint(models, catalog, hint);
301
316
  return provider;
302
317
  }
303
318
  /**
@@ -339,8 +354,13 @@ export async function cmdQuickAdd(opts) {
339
354
  const gatewayLoader = sharedGatewayLoader();
340
355
  for (const protocol of protocols) {
341
356
  const explicitId = opts.id === undefined ? undefined : `${opts.id}${multi ? `-${protocol}` : ""}`;
342
- const existing = explicitId === undefined ? findMatchingProvider(Object.values(store.providers), { protocol, baseUrl, apiKey }) : store.providers[explicitId];
343
- const id = explicitId ?? existing?.id ?? availableProviderId(providerIdFromBaseUrl(baseUrl, protocol), store.providers);
357
+ const byId = explicitId ? store.providers[explicitId] : undefined;
358
+ const sameAccount = byId ? undefined : findMatchingProvider(Object.values(store.providers), { protocol, baseUrl, apiKey });
359
+ const existing = byId ?? sameAccount;
360
+ if (byId === undefined && explicitId && sameAccount) {
361
+ console.log(pc.yellow(t("add.alreadyConfigured", { id: pc.bold(sameAccount.id) })));
362
+ }
363
+ const id = byId?.id ?? sameAccount?.id ?? explicitId ?? availableProviderId(providerIdFromBaseUrl(baseUrl, protocol), store.providers);
344
364
  const name = opts.name ?? existing?.name ?? (opts.id === undefined ? providerNameFromBaseUrl(baseUrl, protocol) : multi ? `${opts.id} (${protocol})` : opts.id);
345
365
  const modelFilter = parseFilterOpts(opts, existing?.modelFilter);
346
366
  process.stderr.write(pc.dim(`${t("add.discovering", { url: baseUrl })} [${protocol}]\n`));
@@ -641,7 +661,7 @@ export async function cmdModels(query, opts) {
641
661
  }
642
662
  console.log(`${pc.bold(provider.id)} (${provider.protocol}) · ${provider.baseUrl}`);
643
663
  console.log(table(modelRows(provider.models), MODEL_HEADER));
644
- printUncatalogedHint(provider.models);
664
+ printUncatalogedHint(provider.models, await loadCatalog(), provider.modelsDevId);
645
665
  return;
646
666
  }
647
667
  const catalog = await loadCatalog({ refresh: opts.refresh });
@@ -680,9 +700,11 @@ export async function cmdRefreshMeta(opts = {}) {
680
700
  updated++;
681
701
  }
682
702
  saveStore(store);
683
- console.log(pc.green(`checked model metadata (${updated} provider(s) changed)`));
703
+ console.log(pc.green(t("refresh.checked", { changed: updated })));
684
704
  if (updated > 0)
685
- console.log(pc.dim("run `agentsw sync` to push updated metadata into app configs"));
705
+ console.log(pc.dim(t("refresh.next")));
706
+ for (const provider of providers)
707
+ printUncatalogedHint(provider.models, catalog, provider.modelsDevId);
686
708
  }
687
709
  export async function cmdDiscover(id, opts) {
688
710
  resolveTargets(opts.apps);
@@ -715,7 +737,7 @@ export async function cmdDiscover(id, opts) {
715
737
  (added.length ? `\n new: ${added.join(", ")}` : "") +
716
738
  (gone.length ? `\n removed upstream: ${gone.join(", ")}` : ""));
717
739
  console.log(table(modelRows(provider.models), MODEL_HEADER));
718
- printUncatalogedHint(provider.models);
740
+ printUncatalogedHint(provider.models, catalog, provider.modelsDevId);
719
741
  if (opts.sync) {
720
742
  console.log("");
721
743
  await cmdSync({ provider: id, apps: opts.apps });
package/dist/i18n.js CHANGED
@@ -190,6 +190,7 @@ const messages = {
190
190
  "add.defaultMissing": { en: "default model {model} is not in the model list", "zh-CN": "默认模型 {model} 不在模型列表中" },
191
191
  "add.smallMissing": { en: "small model {model} is not in the model list", "zh-CN": "小型模型 {model} 不在模型列表中" },
192
192
  "add.added": { en: "added", "zh-CN": "已添加" },
193
+ "add.alreadyConfigured": { en: "same endpoint and key are already configured as {id}; updating it instead of creating a duplicate", "zh-CN": "相同地址与密钥已配置为 {id};将更新该供应商,不会新建重复项" },
193
194
  "add.updated": { en: "updated", "zh-CN": "已更新" },
194
195
  "add.saved": { en: "{status} provider {id} ({protocol})", "zh-CN": "{status}供应商 {id} ({protocol})" },
195
196
  "add.metadata": { en: "model metadata: {matched}/{total} models have a context limit", "zh-CN": "模型参数:{matched}/{total} 个模型已有上下文上限" },
@@ -245,6 +246,18 @@ const messages = {
245
246
  "status.config": { en: "config: {file}", "zh-CN": "配置:{file}" },
246
247
  "status.active": { en: "active provider: {id}", "zh-CN": "当前供应商:{id}" },
247
248
  "status.none": { en: "(none)", "zh-CN": "(无)" },
249
+ "meta.gap": { en: "{count} model(s) have no catalog metadata (shown as \"-\")", "zh-CN": "{count} 个模型没有目录元数据(显示为 \"-\")" },
250
+ "meta.ambiguous": {
251
+ en: "ambiguous ({count}): {ids} — several creators list these; pin with --gateway-models '{\"{id}\":\"creator/model\"}'",
252
+ "zh-CN": "存在歧义({count}):{ids} —— 多个厂商都提供;用 --gateway-models '{\"{id}\":\"creator/model\"}' 指定",
253
+ },
254
+ "meta.unknown": { en: "not listed by any creator ({count}): {ids}", "zh-CN": "所有厂商均未收录({count}):{ids}" },
255
+ "meta.noCreatorRow": {
256
+ en: "no models.dev row for the creator ({count}): {ids} — filled from AI Gateway when available",
257
+ "zh-CN": "厂商在 models.dev 无对应条目({count}):{ids} —— 有 AI Gateway 数据时会自动补全",
258
+ },
259
+ "refresh.checked": { en: "checked model metadata ({changed} provider(s) changed)", "zh-CN": "已检查模型元数据({changed} 个供应商有变化)" },
260
+ "refresh.next": { en: "run `agentsw sync` to push updated metadata into app configs", "zh-CN": "运行 `agentsw sync` 将更新的元数据写入各智能体配置" },
248
261
  };
249
262
  let current = detectSystemLocale();
250
263
  export function normalizeLocale(value) {
package/dist/metadata.js CHANGED
@@ -105,25 +105,42 @@ function equalValue(a, b) {
105
105
  function indexCatalog(catalog) {
106
106
  const exact = new Map();
107
107
  const canonical = new Map();
108
+ const identities = new Map();
109
+ const push = (map, key, entry) => {
110
+ const matches = map.get(key);
111
+ if (matches)
112
+ matches.push(entry);
113
+ else
114
+ map.set(key, [entry]);
115
+ };
108
116
  for (const provider of Object.values(catalog)) {
109
117
  for (const [key, model] of Object.entries(provider.models)) {
110
118
  const modelId = model.id.includes("/") ? model.id : `${provider.id}/${model.id}`;
111
- const entry = { provider: provider.id, modelId, model };
112
- for (const id of new Set([key, model.id])) {
113
- const matches = exact.get(id);
114
- if (matches)
115
- matches.push(entry);
116
- else
117
- exact.set(id, [entry]);
118
- }
119
- const matches = canonical.get(modelId);
120
- if (matches)
121
- matches.push(entry);
122
- else
123
- canonical.set(modelId, [entry]);
119
+ const entry = { provider: provider.id, modelId, identity: model.canonical_model_id ?? modelId, model };
120
+ for (const id of new Set([key, model.id]))
121
+ push(exact, id, entry);
122
+ push(canonical, modelId, entry);
123
+ push(identities, entry.identity, entry);
124
124
  }
125
125
  }
126
- return { exact, canonical };
126
+ return { exact, canonical, identities };
127
+ }
128
+ /** Creator identity of a catalog model id, or undefined when its rows disagree. */
129
+ function catalogIdentity(index, modelId) {
130
+ const identities = new Set(index?.canonical.get(modelId)?.map((entry) => entry.identity));
131
+ return identities?.size === 1 ? [...identities][0] : undefined;
132
+ }
133
+ /**
134
+ * The creator's own listing for an identity, when models.dev carries one.
135
+ * Gateways also key their rows by the canonical id, and those rows carry the
136
+ * gateway's own limits and prices, so only a row from the creator's own
137
+ * namespace counts as authoritative.
138
+ */
139
+ function creatorEntry(index, identity) {
140
+ const slash = identity.indexOf("/");
141
+ if (slash < 1)
142
+ return undefined;
143
+ return index?.identities.get(identity)?.find((entry) => entry.modelId === identity && entry.provider === identity.slice(0, slash));
127
144
  }
128
145
  function indexGatewayBareIds(gateway) {
129
146
  const result = new Map();
@@ -147,10 +164,15 @@ function selectExact(index, id, hint, aliases, gateway, gatewayBareIds) {
147
164
  const candidate = gatewayBareIds?.get(id);
148
165
  const evidence = index?.exact.get(id);
149
166
  const hintedEvidence = hint ? evidence?.filter((entry) => entry.provider === hint) : undefined;
150
- const identities = new Set((hintedEvidence?.length ? hintedEvidence : evidence)?.map((entry) => entry.modelId));
167
+ const identities = new Set((hintedEvidence?.length ? hintedEvidence : evidence)?.map((entry) => entry.identity));
168
+ // Gateway names the creator while models.dev names the listing provider, so
169
+ // the two agree only once both are reduced to a creator identity. A gateway
170
+ // id the catalog has no row for stands for itself.
171
+ const candidateIdentity = candidate ? catalogIdentity(index, candidate) ?? candidate : undefined;
151
172
  // A unique exact basename is safe only with absent or agreeing primary evidence.
152
173
  // Explicit provider hints can resolve primary ambiguity, but never Gateway ambiguity.
153
- if (candidate && (identities.size === 0 || (identities.size === 1 && identities.has(candidate))))
174
+ if (candidate && candidateIdentity !== undefined
175
+ && (identities.size === 0 || (identities.size === 1 && identities.has(candidateIdentity))))
154
176
  gatewayId = candidate;
155
177
  }
156
178
  }
@@ -159,10 +181,10 @@ function selectExact(index, id, hint, aliases, gateway, gatewayBareIds) {
159
181
  : (bare && hint ? index?.canonical.get(`${hint}/${id}`) : undefined)
160
182
  ?? index?.canonical.get(id) ?? index?.exact.get(id);
161
183
  const hinted = candidates?.find((entry) => entry.provider === hint);
162
- const identities = new Set(candidates?.map((entry) => entry.modelId));
163
- const primary = hinted ?? (identities.size === 1
164
- ? candidates?.find((entry) => entry.modelId.startsWith(`${entry.provider}/`)) ?? candidates?.[0]
165
- : undefined);
184
+ const identities = new Set(candidates?.map((entry) => entry.identity));
185
+ // Reseller rows for one model share an identity but disagree on limits and
186
+ // prices, so only the creator's own row is authoritative.
187
+ const primary = hinted ?? (identities.size === 1 ? creatorEntry(index, [...identities][0]) : undefined);
166
188
  return { primary, gatewayId, alias };
167
189
  }
168
190
  function legacyPrimary(catalog, id, hint) {
@@ -398,3 +420,30 @@ export async function enrichProviderModels(catalog, ids, settings, options = {})
398
420
  // bounds must be resolved with the supplement against the original values.
399
421
  return gateway ? enrich(gateway) : primaryOnly ?? enrich();
400
422
  }
423
+ /**
424
+ * Why a model id stayed bare after enrichment, so callers can tell the user
425
+ * what is worth acting on: "ambiguous" ids are listed under more than one
426
+ * creator and an explicit mapping resolves them; "unknown" ids no creator
427
+ * lists at all, so no mapping helps; "noCreatorRow" ids have one unambiguous
428
+ * creator but models.dev carries no authoritative row for it, so only Gateway
429
+ * can fill them.
430
+ */
431
+ export function classifyUnresolved(catalog, ids, hint) {
432
+ const ambiguous = [];
433
+ const unknown = [];
434
+ const noCreatorRow = [];
435
+ if (!catalog)
436
+ return { ambiguous, unknown: [...ids], noCreatorRow };
437
+ const index = indexCatalog(catalog);
438
+ for (const id of ids) {
439
+ const hinted = hint ? index.canonical.get(`${hint}/${id}`) : undefined;
440
+ const candidates = hinted ?? index.canonical.get(id) ?? index.exact.get(id);
441
+ if (!candidates?.length)
442
+ unknown.push(id);
443
+ else if (new Set(candidates.map((entry) => entry.identity)).size > 1)
444
+ ambiguous.push(id);
445
+ else if (!creatorEntry(index, [...new Set(candidates.map((entry) => entry.identity))][0]))
446
+ noCreatorRow.push(id);
447
+ }
448
+ return { ambiguous, unknown, noCreatorRow };
449
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentsw",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
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": {