@bd7pil/reeve 0.1.13 → 0.1.14

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.
@@ -30,7 +30,8 @@
30
30
  "prefer": ["glm-5.3-flash-free", "mimo-v2.6-flash-free", "deepseek-v4.1-flash-free"],
31
31
  "deny": [],
32
32
  "require_image": true,
33
- "image_models": ["glm-5.3-flash-free", "deepseek-v4.1-flash-free", "mimo-v2.6-flash-free", "nemotron-3.5-super-vl-free"],
33
+ "modality_url": "https://models.dev/api.json",
34
+ "modality_ttl_seconds": 21600,
34
35
  "static": {}
35
36
  },
36
37
  "chains": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bd7pil/reeve",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "REEVE 编排层(opencode v2 插件):agent/skill 注册 + 子任务台账 + GATE 代理层(路由/回退/台账)+ 性能统计",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/provision.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema": 1,
3
3
  "name": "reeve",
4
- "version": "0.1.13",
4
+ "version": "0.1.14",
5
5
  "license": "MIT",
6
6
  "//": "REEVE 在 ocrc 实例里的装配说明。**没有二进制工件** —— 代理层是插件内的 GATE(纯 JS),不需要任何本地服务(Bifrost 时代已结束)。",
7
7
  "host": {
@@ -15,7 +15,7 @@
15
15
  ]
16
16
  },
17
17
  "enginePlugin": {
18
- "spec": "@bd7pil/reeve@0.1.13",
18
+ "spec": "@bd7pil/reeve@0.1.14",
19
19
  "kind": "package",
20
20
  "entry": "~/.ocrc/components/reeve/node_modules/@bd7pil/reeve",
21
21
  "requiresPeer": []
@@ -45,9 +45,10 @@ export const FREE_DEFAULTS = Object.freeze({
45
45
  failure_cooldown_seconds: 600,
46
46
  prefer: Object.freeze([]),
47
47
  deny: Object.freeze([]),
48
- // 免费池只要多模态模型:开启后只保留 image_models 里的 ID(纯文本免费模型不进池)
48
+ // 免费池只保留 image-capable 模型:模态从 models.dev 动态读取(不再维护静态白名单)
49
49
  require_image: false,
50
- image_models: Object.freeze([]),
50
+ modality_url: 'https://models.dev/api.json',
51
+ modality_ttl_seconds: 21_600,
51
52
  static: Object.freeze({}),
52
53
  sources: Object.freeze({
53
54
  // 公开模型列表(不需要键;拉不到只影响该 provider,请求照常降级到付费)
@@ -125,7 +126,8 @@ export function normalizeGateConfig(raw) {
125
126
  prefer: asStrings(fSrc.prefer),
126
127
  deny: asStrings(fSrc.deny),
127
128
  require_image: fSrc.require_image === true,
128
- image_models: asStrings(fSrc.image_models),
129
+ modality_url: typeof fSrc.modality_url === 'string' && fSrc.modality_url.length > 0 ? fSrc.modality_url : FREE_DEFAULTS.modality_url,
130
+ modality_ttl_seconds: asInt(fSrc.modality_ttl_seconds, 60, 604_800, FREE_DEFAULTS.modality_ttl_seconds),
129
131
  static: {},
130
132
  sources: { ...FREE_DEFAULTS.sources },
131
133
  }
package/src/gate/free.js CHANGED
@@ -37,6 +37,8 @@ export function createFreeLane({ getConfig, getAuth = () => ({}), now = Date.now
37
37
  /** ref -> { fails: number, until: number }(全局,跨 session:坏模型对所有人都坏) */
38
38
  const cooldowns = new Map()
39
39
  const inflight = new Map()
40
+ /** models.dev 的 modalities:{ at, map: Map<ref, boolean>|null, error, inflight } */
41
+ const modalityCache = { at: 0, map: null, error: null, inflight: null }
40
42
 
41
43
  const freeCfg = () => {
42
44
  const f = getConfig()?.free
@@ -107,6 +109,52 @@ export function createFreeLane({ getConfig, getAuth = () => ({}), now = Date.now
107
109
  return inflight.get(id)
108
110
  }
109
111
 
112
+ /**
113
+ * 免费池的模态元数据(models.dev):只拉一次,TTL 6h;失败保留上一份好数据。
114
+ * 不能用 Zen 的 `/models`:它只有 id/object/created/owned_by,没有 modalities。
115
+ */
116
+ const loadModalities = async () => {
117
+ const free = freeCfg()
118
+ if (free.require_image !== true) return modalityCache
119
+ const ttlMs = Math.max(60, Number(free.modality_ttl_seconds ?? 21_600)) * 1000
120
+ if (modalityCache.map && now() - modalityCache.at < ttlMs) return modalityCache
121
+ if (modalityCache.inflight) return modalityCache.inflight
122
+ const url = typeof free.modality_url === 'string' && free.modality_url.length > 0 ? free.modality_url : 'https://models.dev/api.json'
123
+ modalityCache.inflight = (async () => {
124
+ try {
125
+ const res = await fetchImpl(url, {
126
+ headers: { accept: 'application/json' },
127
+ signal: AbortSignal.timeout(Math.max(timeoutMs, 15_000)),
128
+ })
129
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
130
+ const body = await res.json()
131
+ const map = new Map()
132
+ for (const [pid, provider] of Object.entries(body ?? {})) {
133
+ const models = provider?.models
134
+ if (!models || typeof models !== 'object') continue
135
+ for (const [mid, model] of Object.entries(models)) {
136
+ const input = Array.isArray(model?.modalities?.input)
137
+ ? model.modalities.input
138
+ : Array.isArray(model?.input)
139
+ ? model.input
140
+ : null
141
+ map.set(`${pid}/${mid}`, input ? input.includes('image') : model?.attachment === true)
142
+ }
143
+ }
144
+ modalityCache.map = map
145
+ modalityCache.at = now()
146
+ modalityCache.error = null
147
+ } catch (err) {
148
+ modalityCache.error = String(err?.message ?? err).slice(0, 140)
149
+ logger?.debug?.(`[gate] 免费池模态元数据拉取失败(保留上一份):${modalityCache.error}`)
150
+ } finally {
151
+ modalityCache.inflight = null
152
+ }
153
+ return modalityCache
154
+ })()
155
+ return modalityCache.inflight
156
+ }
157
+
110
158
  const isCooling = (ref) => {
111
159
  const c = cooldowns.get(ref)
112
160
  if (!c) return false
@@ -127,17 +175,22 @@ export function createFreeLane({ getConfig, getAuth = () => ({}), now = Date.now
127
175
  const free = freeCfg()
128
176
  if (!free.enabled) return []
129
177
  const requireImage = free.require_image === true
130
- const imageModels = new Set(asList(free.image_models))
178
+ let modalities = null
179
+ if (requireImage) {
180
+ const m = await loadModalities()
181
+ modalities = m.map
182
+ // 元数据从未成功拿到 → 没有可靠证据证明哪个免费模型能吃图;宁可走付费,也不退回纯文本。
183
+ if (!modalities) return []
184
+ }
131
185
  const out = []
132
186
  for (const id of asList(free.providers)) {
133
187
  const provider = providers()[id]
134
188
  if (!provider?.base_url || !hasKey(provider)) continue
135
189
  const st = await load(id)
136
190
  for (const model of st.models ?? []) {
137
- // 只要多模态免费模型:纯文本的 big-pickle/minimax 等不进池。
138
- // 白名单为空时按“没有可用免费候选”处理(降级到付费),绝不退回纯文本。
139
- if (requireImage && !imageModels.has(model) && !imageModels.has(`${id}/${model}`)) continue
140
- out.push({ provider: id, model, ref: `${id}/${model}` })
191
+ const ref = `${id}/${model}`
192
+ if (requireImage && modalities.get(ref) !== true) continue
193
+ out.push({ provider: id, model, ref })
141
194
  }
142
195
  }
143
196
  return out
@@ -244,6 +297,9 @@ export function createFreeLane({ getConfig, getAuth = () => ({}), now = Date.now
244
297
  cooling,
245
298
  prefer: asList(free.prefer),
246
299
  deny: asList(free.deny),
300
+ require_image: free.require_image === true,
301
+ modality_ready: Boolean(modalityCache.map),
302
+ modality_error: modalityCache.error,
247
303
  }
248
304
  }
249
305
 
@@ -269,6 +325,7 @@ export function createFreeLane({ getConfig, getAuth = () => ({}), now = Date.now
269
325
  const warm = () => {
270
326
  const free = freeCfg()
271
327
  if (free.enabled === false) return
328
+ if (free.require_image === true) void loadModalities().catch(() => {})
272
329
  for (const id of asList(free.providers)) {
273
330
  if (providers()[id]?.base_url && hasKey(providers()[id])) void load(id).catch(() => {})
274
331
  }