dsh-tap 0.20.0

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.
Files changed (121) hide show
  1. package/AGENTS.md +99 -0
  2. package/CHANGELOG.md +751 -0
  3. package/LICENSE +21 -0
  4. package/README.en.md +59 -0
  5. package/README.md +256 -0
  6. package/cordis.patch.yml +385 -0
  7. package/core/bridge.js +698 -0
  8. package/core/json-store.js +91 -0
  9. package/core/rotation.js +108 -0
  10. package/core/usage-meter.js +176 -0
  11. package/docs/diagnosis-cache-quota.md +181 -0
  12. package/docs/diagnosis-qoder-flash.md +67 -0
  13. package/docs/diagnosis-trae-3003.md +302 -0
  14. package/docs/goals/bridge-port-host-split.md +91 -0
  15. package/docs/goals/desktop-adaptation.md +106 -0
  16. package/docs/goals/qoder-cn-provider-design.md +308 -0
  17. package/docs/goals/settings-card-ux-redesign-plan.md +1680 -0
  18. package/docs/goals/settings-card-ux-redesign.md +162 -0
  19. package/docs/goals/trae-agent-v3.md +41 -0
  20. package/docs/goals/trae-work-cn-repair.md +44 -0
  21. package/docs/goals/v0.8-/351/242/235/345/272/246/345/217/257/350/247/201-/346/250/241/345/236/213/345/212/250/346/200/201/345/214/226-/345/244/232/346/234/215/345/212/241/345/225/206.md +85 -0
  22. package/docs/pitfalls.md +105 -0
  23. package/docs/reverse/trae-cloud-api.md +218 -0
  24. package/docs/reverse/trae-model-catalog.md +129 -0
  25. package/docs/reverse/traework-cn.md +520 -0
  26. package/docs/rules/STATE.md +198 -0
  27. package/docs/rules/content-moderation.md +54 -0
  28. package/docs/rules/dev-role-boundary.md +94 -0
  29. package/docs/rules/extra-providers.md +42 -0
  30. package/docs/rules/gateway-facts.md +91 -0
  31. package/docs/rules/oauth-handshake.md +76 -0
  32. package/docs/rules/prompt-cache.md +93 -0
  33. package/docs/rules/quota-signals.md +125 -0
  34. package/docs/rules/routing.md +84 -0
  35. package/docs/rules/templates/oauth-reverse-checklist.md +42 -0
  36. package/docs/rules/trae-surface.md +242 -0
  37. package/docs/rules/ua-validation.md +81 -0
  38. package/host-config.js +282 -0
  39. package/index.js +2306 -0
  40. package/lib/client.js +2893 -0
  41. package/local-scan.js +104 -0
  42. package/package.json +82 -0
  43. package/providers/ark/index.js +11 -0
  44. package/providers/bailian/index.js +10 -0
  45. package/providers/bigmodel/index.js +11 -0
  46. package/providers/codebuddy/agenttool.js +122 -0
  47. package/providers/codebuddy/catalog.js +227 -0
  48. package/providers/codebuddy/errors.js +44 -0
  49. package/providers/codebuddy/headers.js +36 -0
  50. package/providers/codebuddy/images.js +125 -0
  51. package/providers/codebuddy/index.js +123 -0
  52. package/providers/codebuddy/oauth.js +279 -0
  53. package/providers/deepseek/index.js +11 -0
  54. package/providers/moonshot/index.js +11 -0
  55. package/providers/openai-compat.js +177 -0
  56. package/providers/openrouter/index.js +27 -0
  57. package/providers/qoder/catalog.js +145 -0
  58. package/providers/qoder/cosy.js +419 -0
  59. package/providers/qoder/gateway.js +563 -0
  60. package/providers/qoder/index.js +116 -0
  61. package/providers/qoder/oauth.js +364 -0
  62. package/providers/qoder/qoder_auth.wasm +0 -0
  63. package/providers/qoder/quota.js +56 -0
  64. package/providers/qwen/index.js +15 -0
  65. package/providers/tool-pairing.js +129 -0
  66. package/providers/trae/catalog.js +103 -0
  67. package/providers/trae/errors.js +85 -0
  68. package/providers/trae/gateway.js +853 -0
  69. package/providers/trae/index.js +126 -0
  70. package/providers/trae/oauth.js +443 -0
  71. package/providers/trae/quota.js +75 -0
  72. package/providers/trae/remote.js +365 -0
  73. package/scripts/capture-cache.mjs +65 -0
  74. package/scripts/capture-traffic.mjs +83 -0
  75. package/scripts/hermes-probe-dev-role.mjs +263 -0
  76. package/scripts/measure-latency.mjs +253 -0
  77. package/scripts/probe-ark-thinking.mjs +298 -0
  78. package/scripts/probe-cache-decline.mjs +292 -0
  79. package/scripts/probe-cache-ttl.mjs +221 -0
  80. package/scripts/probe-cache.mjs +156 -0
  81. package/scripts/probe-codebuddy-efforts.mjs +355 -0
  82. package/scripts/probe-codebuddy-tier-wiring.mjs +244 -0
  83. package/scripts/probe-effort-gaps.mjs +133 -0
  84. package/scripts/probe-media.mjs +115 -0
  85. package/scripts/probe-moderation.mjs +159 -0
  86. package/scripts/probe-oauth.mjs +617 -0
  87. package/scripts/probe-qoder-attribution-arm8.mjs +92 -0
  88. package/scripts/probe-qoder-attribution-arm9.mjs +102 -0
  89. package/scripts/probe-qoder-attribution.mjs +266 -0
  90. package/scripts/probe-qoder-flash-confirm.mjs +94 -0
  91. package/scripts/probe-qoder-live.mjs +344 -0
  92. package/scripts/probe-qoder-matrix.mjs +274 -0
  93. package/scripts/probe-qoder-null-content.mjs +153 -0
  94. package/scripts/probe-qoder-pairing.mjs +308 -0
  95. package/scripts/probe-qoder-quota.mjs +162 -0
  96. package/scripts/probe-qoder-thinking-config.mjs +52 -0
  97. package/scripts/probe-qoder-thinking-efforts.mjs +269 -0
  98. package/scripts/probe-quota.mjs +136 -0
  99. package/scripts/probe-quota2.mjs +144 -0
  100. package/scripts/probe-routing.mjs +226 -0
  101. package/scripts/probe-trae-3003-diagnosis.mjs +139 -0
  102. package/scripts/probe-trae-agent-v3.mjs +399 -0
  103. package/scripts/probe-trae-efforts.mjs +149 -0
  104. package/scripts/probe-trae-live.mjs +147 -0
  105. package/scripts/probe-trae-max-effort.mjs +179 -0
  106. package/scripts/probe-trae-model-routing.mjs +381 -0
  107. package/scripts/probe-trae-thinking-scene.mjs +238 -0
  108. package/scripts/probe-trae-transport-outage.mjs +176 -0
  109. package/scripts/probe-ua.mjs +508 -0
  110. package/scripts/trae-model-catalog.mjs +632 -0
  111. package/scripts/verify-agents-md.mjs +45 -0
  112. package/scripts/verify-bridge.mjs +1041 -0
  113. package/scripts/verify-core-generic.mjs +302 -0
  114. package/scripts/verify-desktop-acceptance.mjs +184 -0
  115. package/scripts/verify-host-config.mjs +265 -0
  116. package/scripts/verify-models.mjs +390 -0
  117. package/scripts/verify-providers.mjs +255 -0
  118. package/scripts/verify-qoder-provider.mjs +1020 -0
  119. package/scripts/verify-rotation.mjs +308 -0
  120. package/scripts/verify-trae-model-catalog.mjs +284 -0
  121. package/scripts/verify-trae-provider.mjs +1451 -0
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Offline regression for providers/openai-compat.js (G6/G7 shared skeleton):
4
+ *
5
+ * 1. primary path: GET /models works → real catalog returned
6
+ * 2. /models 401 (bad key) → throws, NOT tagged MODELS_ENDPOINT_404
7
+ * 3. /models 404 without fallbackModels → error propagates (strict custom)
8
+ * 4. /models 404 + fallbackModels + good key → chat probe ok → fallback list
9
+ * 5. /models 404 + fallbackModels + bad key (std 401 invalid_api_key) → throws
10
+ * 6. iFlow dialect: 200 + {"status":"434"} → auth fail; 200 + choices → ok
11
+ * 7. providerBlock shape (apiKeyEnv convention)
12
+ * 8. shipped presets: baseURL 钉选;qwen 兜底、openrouter staticCatalog
13
+ * 9. staticCatalog 形态:公开 /models 不调不验,chat 探针验 key,吃内置清单
14
+ * 10. refresh 重建接缝(0.19.0 修):重建 adapter 重铺块后档位声明仍在
15
+ * (mock 上游走 refresh 实际序列)+ index.js test/refresh 调用点源级断言
16
+ *
17
+ * Usage: node scripts/verify-providers.mjs (no network, no credentials)
18
+ */
19
+
20
+ import { createServer } from 'node:http'
21
+ import { readFileSync } from 'node:fs'
22
+
23
+ import { PROVIDER_ID_RE, keyRefFor, fetchOpenAIModels, probeChatKey, providerBlock, createOpenAICompatProvider, rebuildAdapterForEntry } from '../providers/openai-compat.js'
24
+ import ark from '../providers/ark/index.js'
25
+ import bailian from '../providers/bailian/index.js'
26
+ import deepseek from '../providers/deepseek/index.js'
27
+ import bigmodel from '../providers/bigmodel/index.js'
28
+ import moonshot from '../providers/moonshot/index.js'
29
+ import openrouter from '../providers/openrouter/index.js'
30
+ import qwen from '../providers/qwen/index.js'
31
+
32
+ let failures = 0
33
+ function check(label, cond, detail = '') {
34
+ if (cond) console.log(` ok ${label}`)
35
+ else { failures++; console.log(` FAIL ${label}${detail ? ` — ${detail}` : ''}`) }
36
+ }
37
+
38
+ /** Spin up a mock upstream; routes: { models: [status, body], chat: (req, body) => [status, body] }. */
39
+ function mockUpstream(routes) {
40
+ const server = createServer((req, res) => {
41
+ let raw = ''
42
+ req.on('data', (c) => { raw += c })
43
+ req.on('end', () => {
44
+ const hit = req.url.endsWith('/models') ? routes.models
45
+ : req.url.endsWith('/chat/completions') ? (typeof routes.chat === 'function' ? routes.chat(req, raw) : routes.chat)
46
+ : null
47
+ if (!hit) { res.writeHead(404); res.end('nf'); return }
48
+ res.writeHead(hit[0], { 'content-type': 'application/json' })
49
+ res.end(typeof hit[1] === 'string' ? hit[1] : JSON.stringify(hit[1]))
50
+ })
51
+ })
52
+ return new Promise((resolve) => server.listen(0, '127.0.0.1', () => {
53
+ resolve({ server, base: `http://127.0.0.1:${server.address().port}/v1` })
54
+ }))
55
+ }
56
+
57
+ const GOOD = 'sk-good-key'
58
+
59
+ // 1–3: /models behavior variants
60
+ {
61
+ const { server, base } = await mockUpstream({
62
+ models: [200, { object: 'list', data: [{ id: 'm-a' }, { id: 'm-b', name: 'B' }] }],
63
+ chat: [200, { choices: [] }],
64
+ })
65
+ const models = await fetchOpenAIModels(base, GOOD)
66
+ check('1 GET /models 主路径返回真实目录', models.length === 2 && models[0].id === 'm-a' && models[1].id === 'm-b', JSON.stringify(models))
67
+ server.close()
68
+ }
69
+ {
70
+ const { server, base } = await mockUpstream({ models: [401, { error: { message: 'bad key' } }], chat: [200, {}] })
71
+ const err = await fetchOpenAIModels(base, 'sk-bad').then(() => null, (e) => e)
72
+ check('2 /models 401 抛错且不带 404 标记', !!err && err.code !== 'MODELS_ENDPOINT_404' && /401/.test(err.message), String(err))
73
+ server.close()
74
+ }
75
+ {
76
+ const { server, base } = await mockUpstream({ models: [404, 'nf'], chat: [200, { choices: [] }] })
77
+ const strict = createOpenAICompatProvider({ id: 'strict', displayName: 'Strict', baseURL: base })
78
+ const err = await strict.fetchModels(GOOD).then(() => null, (e) => e)
79
+ check('3 无 fallbackModels 时 /models 404 原样抛出(custom 严格)', err?.code === 'MODELS_ENDPOINT_404', String(err))
80
+ server.close()
81
+ }
82
+
83
+ // 4–5: fallback path, standard auth dialect
84
+ {
85
+ const { server, base } = await mockUpstream({
86
+ models: [404, 'nf'],
87
+ chat: (req) => (req.headers.authorization === `Bearer ${GOOD}`
88
+ ? [200, { choices: [{ message: { content: 'p' } }], usage: { total_tokens: 1 } }]
89
+ : [401, { error: { code: 'invalid_api_key', message: 'invalid access token or token expired' } }]),
90
+ })
91
+ const adapter = createOpenAICompatProvider({ id: 'fb', displayName: 'FB', baseURL: base, fallbackModels: ['fb-pro', 'fb-lite'] })
92
+ const models = await adapter.fetchModels(GOOD)
93
+ check('4 /models 404 + 好 key:探针通过,吃兜底清单', models.length === 2 && models[0].id === 'fb-pro', JSON.stringify(models))
94
+ const err = await adapter.fetchModels('sk-bad').then(() => null, (e) => e)
95
+ check('5 /models 404 + 坏 key(401 invalid_api_key):拒绝', !!err && /认证拒绝|401/.test(err.message), String(err))
96
+ server.close()
97
+ }
98
+
99
+ // 6: iFlow dialect (HTTP 200 + status 434)
100
+ {
101
+ const { server, base } = await mockUpstream({
102
+ models: [404, 'nf'],
103
+ chat: (req) => (req.headers.authorization === `Bearer ${GOOD}`
104
+ ? [200, { status: '200', body: { choices: [{ message: { content: 'p' } }] } }]
105
+ : [200, { status: '434', msg: 'Invalid apiKey,get your apiKey', body: null }]),
106
+ })
107
+ const adapter = createOpenAICompatProvider({ id: 'iflowish', displayName: 'IF', baseURL: base, fallbackModels: ['if-a'] })
108
+ const err = await adapter.fetchModels('sk-bad').then(() => null, (e) => e)
109
+ check('6a iFlow 方言坏 key(200+status 434):拒绝', !!err && /认证拒绝/.test(err.message), String(err))
110
+ const models = await adapter.fetchModels(GOOD)
111
+ check('6b iFlow 方言好 key(200+choices):通过', models.length === 1 && models[0].id === 'if-a', JSON.stringify(models))
112
+ // probeChatKey 直连形态也锁一下(网络错误之外的判定都在这层)。
113
+ await probeChatKey(base, GOOD, 'if-a')
114
+ check('6c probeChatKey 好 key 不抛', true)
115
+ server.close()
116
+ }
117
+
118
+ // 7: providerBlock shape
119
+ {
120
+ const block = providerBlock({ id: 'mock-up', displayName: 'Mock', baseURL: 'http://x/v1' }, [{ id: 'm1' }, { id: 'm2', name: 'N' }])
121
+ check('7 providerBlock:apiKeyEnv 惯例 + 模型形状',
122
+ block.api === 'openai-completions' && block.apiKeyEnv === 'MOCK_UP_API_KEY'
123
+ && block.models.length === 2 && block.models[1].name === 'N' && !('name' in block.models[0]),
124
+ JSON.stringify(block))
125
+ check('7b keyRefFor / PROVIDER_ID_RE 不变', keyRefFor('ark') === 'ARK_API_KEY' && PROVIDER_ID_RE.test('a-1') && !PROVIDER_ID_RE.test('Bad'))
126
+ // 7c 思考档位声明透传(0.19.0):preset.modelEfforts 按模型 id 合并进条目,
127
+ // 未声明的模型保持原形状;reasoningCompat 才写 compat(缺它 pi-ai 出站不写
128
+ // reasoning_effort)。refresh 走同一函数 ⇒ 声明不丢(下面 7e 用缩减清单复验)。
129
+ const preset7 = {
130
+ id: 'mock-up', displayName: 'Mock', baseURL: 'http://x/v1',
131
+ modelEfforts: { m1: { low: 'low', high: 'high' } },
132
+ reasoningCompat: true,
133
+ }
134
+ const block7c = providerBlock(preset7, [{ id: 'm1' }, { id: 'm2', name: 'N' }])
135
+ check('7c providerBlock:声明模型带 reasoningEfforts、未声明的不带',
136
+ JSON.stringify(block7c.models[0].reasoningEfforts) === JSON.stringify({ low: 'low', high: 'high' })
137
+ && block7c.models[1].reasoningEfforts === undefined,
138
+ JSON.stringify(block7c.models))
139
+ check('7d providerBlock:reasoningCompat → compat.supportsReasoningEffort',
140
+ JSON.stringify(block7c.compat) === JSON.stringify({ thinkingFormat: 'openai', supportsReasoningEffort: true })
141
+ && providerBlock({ id: 'mock-up', displayName: 'Mock', baseURL: 'http://x/v1' }, [{ id: 'm1' }]).compat === undefined,
142
+ JSON.stringify(block7c.compat))
143
+ const block7e = providerBlock(preset7, [{ id: 'm1' }])
144
+ check('7e 刷新(缩减/重建清单)后声明按 id 保留',
145
+ JSON.stringify(block7e.models[0].reasoningEfforts) === JSON.stringify({ low: 'low', high: 'high' })
146
+ && block7e.models.length === 1,
147
+ JSON.stringify(block7e.models))
148
+ }
149
+
150
+ // 8: shipped presets
151
+ {
152
+ check('8a ark/bailian/deepseek/bigmodel/moonshot 无兜底(有真 /models)',
153
+ [ark, bailian, deepseek, bigmodel, moonshot].every((p) => p.fallbackModels.length === 0 && !p.staticCatalog))
154
+ check('8b qwen 带兜底清单且首项可作探针模型',
155
+ qwen.fallbackModels.length >= 1 && typeof qwen.fallbackModels[0] === 'string')
156
+ check('8c preset baseURL 不被意外改动',
157
+ ark.baseURL === 'https://ark.cn-beijing.volces.com/api/v3'
158
+ && bailian.baseURL === 'https://dashscope.aliyuncs.com/compatible-mode/v1'
159
+ && deepseek.baseURL === 'https://api.deepseek.com/v1'
160
+ && bigmodel.baseURL === 'https://open.bigmodel.cn/api/paas/v4'
161
+ && moonshot.baseURL === 'https://api.moonshot.cn/v1'
162
+ && openrouter.baseURL === 'https://openrouter.ai/api/v1'
163
+ && qwen.baseURL === 'https://portal.qwen.ai/v1')
164
+ check('8d openrouter 静态目录:staticCatalog + 内置精选清单(id 含 / 且去重)',
165
+ openrouter.staticCatalog === true && openrouter.fallbackModels.length >= 5
166
+ && openrouter.fallbackModels.every((m) => m.includes('/'))
167
+ && new Set(openrouter.fallbackModels).size === openrouter.fallbackModels.length)
168
+ }
169
+
170
+ // 9: staticCatalog(公开目录型上游,OpenRouter 形态):/models 对任意 key 都 200,
171
+ // 目录不能验 key —— 必须走 chat 探针,且清单恒为内置表(不吃 /models 全量)。
172
+ {
173
+ let modelsHits = 0
174
+ const server = createServer((req, res) => {
175
+ if (req.url.endsWith('/models')) {
176
+ modelsHits++
177
+ res.writeHead(200, { 'content-type': 'application/json' })
178
+ res.end(JSON.stringify({ object: 'list', data: [{ id: 'pub-a' }, { id: 'pub-b' }] }))
179
+ return
180
+ }
181
+ let raw = ''
182
+ req.on('data', (c) => { raw += c })
183
+ req.on('end', () => {
184
+ const ok = req.headers.authorization === `Bearer ${GOOD}`
185
+ res.writeHead(ok ? 200 : 401, { 'content-type': 'application/json' })
186
+ res.end(ok ? JSON.stringify({ choices: [] }) : JSON.stringify({ error: { message: 'User not found.', code: 401 } }))
187
+ })
188
+ })
189
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
190
+ const base = `http://127.0.0.1:${server.address().port}/v1`
191
+ const adapter = createOpenAICompatProvider({ id: 'pubcat', displayName: 'Pub', baseURL: base, staticCatalog: true, fallbackModels: ['curated-a', 'curated-b'] })
192
+ const models = await adapter.fetchModels(GOOD)
193
+ check('9a staticCatalog 好 key:清单 = 内置表(不吃公开 /models)',
194
+ models.length === 2 && models[0].id === 'curated-a' && models[1].id === 'curated-b', JSON.stringify(models))
195
+ check('9b staticCatalog 不调用 /models', modelsHits === 0, `modelsHits=${modelsHits}`)
196
+ const err = await adapter.fetchModels('sk-bad').then(() => null, (e) => e)
197
+ check('9c staticCatalog 坏 key(chat 401):拒绝', !!err && /认证拒绝|401/.test(err.message), String(err))
198
+ server.close()
199
+ }
200
+
201
+ // 10: refresh 重建接缝(0.19.0 修):refreshExtraProviderModels 用重建的 adapter
202
+ // 重铺 provider 块,preset 的档位声明(modelEfforts/reasoningCompat)必须
203
+ // 随行。refresh 在组合根内且 PROVIDER_PRESETS 模块私有——shipped preset 也
204
+ // 无一实际声明档位(踩坑 #42 纪律不臆造)⇒ 全链路 mock 造不出带声明的条目,
205
+ // 故在此按 refresh 的实际序列(entry+preset 重建 adapter → fetchModels →
206
+ // modelBlock)打 mock 上游做功能断言;index.js 调用点用源级断言锁住
207
+ // (verify-desktop-acceptance §4a 同口径)。
208
+ {
209
+ const preset10 = {
210
+ id: 'mock-up', displayName: 'Mock', baseURL: 'http://unused.example/v1',
211
+ modelEfforts: { 'm-a': { low: 'low', high: 'high' } },
212
+ reasoningCompat: true,
213
+ }
214
+ const { server, base } = await mockUpstream({
215
+ models: [200, { object: 'list', data: [{ id: 'm-a' }, { id: 'm-b' }] }],
216
+ chat: [200, { choices: [] }],
217
+ })
218
+ // refresh 的序列:registry entry(添加时快照打底)+ preset 先验 → 重建
219
+ // adapter → 实拉目录 → 重铺 provider 块。baseURL 由 entry 供给(merged 对象),
220
+ // preset10 的 baseURL 不参与(与线上 refresh 同构)。
221
+ const entry10 = { id: 'mock-up', displayName: 'Mock', baseURL: base, preset: 'mock-up', keyRef: 'MOCK_UP_API_KEY', addedAt: 1 }
222
+ const adapter10 = rebuildAdapterForEntry(entry10, preset10)
223
+ const models10 = await adapter10.fetchModels(GOOD)
224
+ const block10 = adapter10.modelBlock(models10)
225
+ check('10a refresh 序列(重建 adapter → fetchModels → 重铺块)后声明仍在',
226
+ models10.length === 2
227
+ && JSON.stringify(block10.models[0].reasoningEfforts) === JSON.stringify({ low: 'low', high: 'high' })
228
+ && block10.models[1].reasoningEfforts === undefined
229
+ && JSON.stringify(block10.compat) === JSON.stringify({ thinkingFormat: 'openai', supportsReasoningEffort: true }),
230
+ JSON.stringify(block10))
231
+ server.close()
232
+ const block10b = rebuildAdapterForEntry({ ...entry10, baseURL: 'http://x/v1' }, null).modelBlock([{ id: 'm-a' }])
233
+ check('10b 重建接缝:custom 条目(preset 未命中)不投毒声明',
234
+ block10b.models[0].reasoningEfforts === undefined && block10b.compat === undefined,
235
+ JSON.stringify(block10b))
236
+
237
+ const tapSrc = readFileSync(new URL('../index.js', import.meta.url), 'utf8')
238
+ const fnSrc = (name) => {
239
+ const at = tapSrc.indexOf(`async function ${name}`)
240
+ return at < 0 ? '' : tapSrc.slice(at, at + tapSrc.slice(at).indexOf('\n}'))
241
+ }
242
+ const refreshSrc = fnSrc('refreshExtraProviderModels')
243
+ const callLine = 'const adapter = rebuildAdapterForEntry(entry, presetHit)'
244
+ check('10c index.js refresh 调用点:走共用接缝并重铺 provider 块(防漏传回归)',
245
+ refreshSrc.includes(callLine)
246
+ && refreshSrc.includes('writeProviderBlock(id, adapter.modelBlock(models))')
247
+ && !refreshSrc.includes('createOpenAICompatProvider({ ...entry'),
248
+ 'refreshExtraProviderModels 未走接缝或未重铺块')
249
+ check('10d index.js test 调用点:与 refresh 同参(接缝唯一出处)',
250
+ fnSrc('testExtraProvider').includes(callLine),
251
+ 'testExtraProvider 未走接缝')
252
+ }
253
+
254
+ console.log(failures ? `\n${failures} FAIL` : '\nall green')
255
+ process.exit(failures ? 1 : 0)