@justsilver/opencode-providers 0.3.3 → 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 +10 -0
- package/package.json +1 -1
- package/plugin/opencode-providers/index.ts +63 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@
|
|
|
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
|
+
|
|
10
20
|
## [0.3.3] - 2026-10-08
|
|
11
21
|
|
|
12
22
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justsilver/opencode-providers",
|
|
3
|
-
"version": "0.3.
|
|
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
|
-
|
|
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>
|