@justsilver/opencode-providers 0.3.2 → 0.3.4

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/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.4] - 2026-10-09
11
+
12
+ ### Added
13
+
14
+ - 插件:**注册表后台自动刷新**——服务端入口 `setup()` 之后每 **30 分钟**做一次**非 force** 的 TTL 检查(TTL 仍 6h,6h 内只读缓存零网络);**只有内容真变了(`source === "network"`)才** `integration.reload()` + `provider.reload()`,未变不碰 `/models`;上游不可达时保留现有注册、下个 tick 重试(**启动时拉不到也能自行恢复,无需重启**)。定时器 `unref()`,且 `setup()` **返回 cleanup**(热重载 / location 关闭时 `clearInterval`,**不泄漏**);间隔可用 `options.refreshIntervalMs` 覆盖(≤0 禁用)。
15
+
16
+ ### Changed
17
+
18
+ - 技能 CLI:`search` 由「任一 token 命中」(OR)改为**全 token AND**——归一化后查询的**全部**显著 token 都命中才算,**无兜底**(AND 零命中直接「没有匹配」,不回退宽松)。专治系列词跨家族误召:搜 `glm-5.3-flash` 不再因共有 token `flash` 把 `deepseek-v4.1-flash` 一并召回;单个系列词查询(如 `flash`)仍召回全部家族;大小写不敏感。
19
+
20
+ ## [0.3.3] - 2026-10-08
21
+
22
+ ### Changed
23
+
24
+ - 技能 CLI:`list` / `list --json` 的**顶层共享模型引用方按供应商去重**(同一家多个模型引用同一 canon 时,不再出现 `r4-coder, r4-coder`)。
25
+ - 技能 CLI:`search` 输出给**供应商 id/名称命中**加标记 `〔供应商 id/名称命中〕`,与「仅其名下模型命中」区分。
26
+ - 技能 CLI:`remove-shared-model` 的**拒绝文案**改为**推荐 `remove-model`**,并说明「纯 `base` 引用直接 `--unset base` 会留下无 limit 的非法模型」。
27
+ - 技能:澄清「**精确命中不用问**,直接 `--base`;**模糊命中(多个/相似)**才用 `"multiple"` 的 `question` 二次确认」;「改名」限定为 **id/key(标识)**(**显示名**走 `set-provider --name` / `set-model --model-name`);判定供应商是否存在**用 `show <id>`**(`search` 是宽松召回,同 token 会假阳性);`set-model` / `set-shared-model` **不能补 `variants`**。
28
+ - 技能 CLI:`search` 改为**宽松匹配(召回优先)**——归一化(大小写、`. _ / \` 与 `-` 等价、词序无关、忽略版本数字)+ token,**一次搜索可能召回多个候选**。
29
+ - 技能:命中**分档**——归一化后相等才 `--base`;**召回多个 / 相似但不同**(如 `glm-5` vs `glm-5-air`)→ 用 `"multiple": true` 的 `question` 把候选动态列出让用户**二次确认**(**模糊命中不标「推荐」**);无命中的「家族标签(lab)」题**动态列出已有 lab** 且**支持多选**;并明确**跨步骤不得沿用上一步的 lab**。
30
+ - 插件:模型**输入模态兜底 `["text"]` → `["text","image"]`**,与 opencode 内核 `Model.Info.default()`(`packages/schema/src/model.ts`)一致;注册表未写 `input` 的模型从此默认支持图片,**纯文本模型必须显式写 `input: ["text"]`**(技能 CLI 传 `--input text`),`output` 兜底仍为 `["text"]`。
31
+
10
32
  ## [0.3.2] - 2026-10-08
11
33
 
12
34
  首个稳定版(并入 `0.3.2-beta.0` / `0.3.2-beta.1` 的全部内容)。
@@ -207,7 +229,8 @@
207
229
  - 插件源码从 `.opencode/plugins/opencode-providers/` 移到 `plugin/opencode-providers/`:仓库自身不再是插件发现根,避免在仓库里跑 opencode 时与全局安装的同 id 副本相撞(`Duplicate plugin ID` → 面板里一条 `failed`)
208
230
  - 安装后**通常无需重启**(插件目录被文件监视热重载),不再要求 `opencode service restart`
209
231
 
210
- [Unreleased]: https://github.com/Just-Silver/opencode-providers/compare/v0.3.2...HEAD
232
+ [Unreleased]: https://github.com/Just-Silver/opencode-providers/compare/v0.3.3...HEAD
233
+ [0.3.3]: https://github.com/Just-Silver/opencode-providers/releases/tag/v0.3.3
211
234
  [0.3.2]: https://github.com/Just-Silver/opencode-providers/releases/tag/v0.3.2
212
235
  [0.3.1]: https://github.com/Just-Silver/opencode-providers/releases/tag/v0.3.1
213
236
  [0.3.0]: https://github.com/Just-Silver/opencode-providers/releases/tag/v0.3.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justsilver/opencode-providers",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "description": "Register providers missing from the models.dev catalog in OpenCode V2 from a self-hosted registry — no opencode.json needed",
6
6
  "license": "MIT",
@@ -35,6 +35,16 @@ const PLUGIN_ID = "opencode-providers"
35
35
  /** Tags integrations so the TUI entry can recognise its own (TUI side keeps its own copy of this string). */
36
36
  const INTEGRATION_SOURCE = PLUGIN_ID
37
37
 
38
+ /**
39
+ * Background revalidation cadence. `loadRegistry` (no `force`) still gates the
40
+ * network on the 6h TTL; this only decides how often we *check*, so a stale
41
+ * cache is refreshed within one interval of expiring instead of only on the next
42
+ * plugin load (opencode start / plugin install / config reload / service restart).
43
+ * Overridable via `options.refreshIntervalMs` (tests / forks); a non-positive
44
+ * value disables the timer.
45
+ */
46
+ const DEFAULT_REFRESH_INTERVAL_MS = 30 * 60 * 1000
47
+
38
48
  interface IntegrationRefLike {
39
49
  id: string
40
50
  name: string
@@ -92,6 +102,14 @@ export default {
92
102
  // Closure-local: repeated `setup` calls (config hot reload) must not share state.
93
103
  const state: { registry?: Registry } = {}
94
104
 
105
+ // Swap the closure state and re-run the transforms. Registration is a full
106
+ // replace, so entries removed upstream also disappear on the next apply.
107
+ const applyRegistry = async (registry: Registry): Promise<void> => {
108
+ state.registry = registry
109
+ await ctx.integration.reload()
110
+ await ctx.provider.reload()
111
+ }
112
+
95
113
  // Force refresh: bypass the TTL, re-register, and tell the TUI what it got.
96
114
  // A `stale-cache` result means upstream was unreachable, so it counts as a
97
115
  // failure and leaves `state` (and the registered providers) untouched.
@@ -101,9 +119,7 @@ export default {
101
119
  if (result.source === "stale-cache") {
102
120
  return { ok: false, errors: ["registry refresh failed; serving the cached copy"] }
103
121
  }
104
- state.registry = result.registry
105
- await ctx.integration.reload()
106
- await ctx.provider.reload()
122
+ await applyRegistry(result.registry)
107
123
  return {
108
124
  ok: true,
109
125
  providers: Object.keys(result.registry.providers).length,
@@ -180,6 +196,43 @@ export default {
180
196
  })
181
197
  }
182
198
  })
199
+
200
+ // Background revalidation. A non-force load runs the same TTL check the boot
201
+ // path used; only a real content change (`source === "network"`) re-registers,
202
+ // so an unchanged registry never nudges `provider.reload()` and `/models` does
203
+ // not flap. A failed check keeps the current registration and retries next tick,
204
+ // so a registry that was unavailable at boot recovers without a restart.
205
+ const intervalMs =
206
+ typeof ctx.options.refreshIntervalMs === "number" ? ctx.options.refreshIntervalMs : DEFAULT_REFRESH_INTERVAL_MS
207
+ let timer: ReturnType<typeof setInterval> | undefined
208
+ if (intervalMs > 0) {
209
+ let ticking = false
210
+ const tick = async (): Promise<void> => {
211
+ if (ticking) return
212
+ ticking = true
213
+ try {
214
+ const result = await loadRegistry({ url, fetch: globalThis.fetch, store })
215
+ if (!result.ok || result.source !== "network") return
216
+ await applyRegistry(result.registry)
217
+ } catch (error) {
218
+ console.warn(
219
+ `[${PLUGIN_ID}] background registry refresh failed: ${error instanceof Error ? error.message : String(error)}`,
220
+ )
221
+ } finally {
222
+ ticking = false
223
+ }
224
+ }
225
+ timer = setInterval(() => void tick(), intervalMs)
226
+ // Never keep the process alive for this: the service owns an HTTP listener,
227
+ // and an uncleared timer would otherwise hang `node --test`.
228
+ unrefTimer(timer)
229
+ }
230
+
231
+ // Teardown: the host runs this on unload / hot reload / location close, so a
232
+ // reloaded plugin never leaves an orphaned interval behind.
233
+ return () => {
234
+ if (timer !== undefined) clearInterval(timer)
235
+ }
183
236
  },
184
237
  }
185
238
 
@@ -189,6 +242,13 @@ function countModels(registry: Registry): number {
189
242
  return total
190
243
  }
191
244
 
245
+ /** Detach a timer from the event loop where the runtime supports it (Node/Bun). */
246
+ function unrefTimer(timer: unknown): void {
247
+ if (typeof timer !== "object" || timer === null) return
248
+ const unref = (timer as { unref?: unknown }).unref
249
+ if (typeof unref === "function") (unref as () => void).call(timer)
250
+ }
251
+
192
252
  function asCacheEntry(value: unknown): RegistryCacheEntry | undefined {
193
253
  if (typeof value !== "object" || value === null) return undefined
194
254
  const entry = value as Partial<RegistryCacheEntry>
@@ -56,7 +56,8 @@ export function buildProviderModels(
56
56
  ...(Object.keys(compatibility).length === 0 ? {} : { compatibility }),
57
57
  capabilities: {
58
58
  tools: resolved.tools ?? true,
59
- input: resolved.input ?? ["text"],
59
+ // 与 opencode 内核 `Model.Info.default()` 一致(packages/schema/src/model.ts):缺省 input = text+image。
60
+ input: resolved.input ?? ["text", "image"],
60
61
  output: resolved.output ?? ["text"],
61
62
  },
62
63
  variants: (resolved.variants ?? []).map((variant) =>