@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.
|
|
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.
|
|
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>
|
|
@@ -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
|
-
|
|
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) =>
|