w-dispatch-ai 1.0.6 → 1.0.7
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/README.md +4 -1
- package/dist/w-dispatch-ai.umd.js +2 -2
- package/dist/w-dispatch-ai.umd.js.map +1 -1
- package/docs/WDispatchAi.mjs.html +1 -1
- package/docs/adapters.mjs.html +1 -1
- package/docs/dfTimeoutMs.mjs.html +1 -1
- package/docs/dispatchAi.mjs.html +1 -1
- package/docs/dispatchAiFallback.mjs.html +71 -6
- package/docs/dispatchAiWkf.mjs.html +1 -1
- package/docs/dispatchAntigravity.mjs.html +1 -1
- package/docs/dispatchApiOpenaiCompat.mjs.html +1 -1
- package/docs/dispatchClaude.mjs.html +1 -1
- package/docs/dispatchCodex.mjs.html +1 -1
- package/docs/dispatchOpencode.mjs.html +1 -1
- package/docs/getCliArgs.mjs.html +1 -1
- package/docs/getErrorResult.mjs.html +1 -1
- package/docs/global.html +48 -8
- package/docs/index.html +1 -1
- package/docs/resolveProviders.mjs.html +1 -1
- package/docs/wkf_callAiWithFallback.mjs.html +1 -1
- package/docs/wkf_extractJsonLoose.mjs.html +1 -1
- package/docs/wkf_runFanout.mjs.html +11 -8
- package/docs/wkf_runFanoutPipeline.mjs.html +2 -2
- package/docs/wkf_runRolePipeline.mjs.html +1 -1
- package/package.json +1 -1
- package/src/dispatchAiFallback.mjs +70 -5
- package/src/wkf/runFanout.mjs +10 -7
- package/src/wkf/runFanoutPipeline.mjs +1 -1
- package/test/unit-dispatchAiFallback.test.mjs +95 -0
- package/test/unit-dispatchApiOpenaiCompat.test.mjs +28 -0
- package/test/unit-runFanout.test.mjs +46 -0
|
@@ -504,7 +504,7 @@ export default dispatchApiOpenaiCompat
|
|
|
504
504
|
<br class="clear">
|
|
505
505
|
|
|
506
506
|
<footer>
|
|
507
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026
|
|
507
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026 15:11:06 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
508
508
|
</footer>
|
|
509
509
|
|
|
510
510
|
<script>prettyPrint();</script>
|
|
@@ -190,7 +190,7 @@ export default dispatchClaude
|
|
|
190
190
|
<br class="clear">
|
|
191
191
|
|
|
192
192
|
<footer>
|
|
193
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026
|
|
193
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026 15:11:06 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
194
194
|
</footer>
|
|
195
195
|
|
|
196
196
|
<script>prettyPrint();</script>
|
|
@@ -191,7 +191,7 @@ export default dispatchCodex
|
|
|
191
191
|
<br class="clear">
|
|
192
192
|
|
|
193
193
|
<footer>
|
|
194
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026
|
|
194
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026 15:11:06 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
195
195
|
</footer>
|
|
196
196
|
|
|
197
197
|
<script>prettyPrint();</script>
|
|
@@ -260,7 +260,7 @@ export default dispatchOpencode
|
|
|
260
260
|
<br class="clear">
|
|
261
261
|
|
|
262
262
|
<footer>
|
|
263
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026
|
|
263
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026 15:11:06 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
264
264
|
</footer>
|
|
265
265
|
|
|
266
266
|
<script>prettyPrint();</script>
|
package/docs/getCliArgs.mjs.html
CHANGED
|
@@ -113,7 +113,7 @@ export default getCliArgs
|
|
|
113
113
|
<br class="clear">
|
|
114
114
|
|
|
115
115
|
<footer>
|
|
116
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026
|
|
116
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026 15:11:06 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
117
117
|
</footer>
|
|
118
118
|
|
|
119
119
|
<script>prettyPrint();</script>
|
|
@@ -102,7 +102,7 @@ export default getErrorResult
|
|
|
102
102
|
<br class="clear">
|
|
103
103
|
|
|
104
104
|
<footer>
|
|
105
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026
|
|
105
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026 15:11:06 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
106
106
|
</footer>
|
|
107
107
|
|
|
108
108
|
<script>prettyPrint();</script>
|
package/docs/global.html
CHANGED
|
@@ -2278,7 +2278,7 @@ providers陣列順序即優先序,排前面的先用;
|
|
|
2278
2278
|
|
|
2279
2279
|
<dt class="tag-source">Source:</dt>
|
|
2280
2280
|
<dd class="tag-source"><ul class="dummy"><li>
|
|
2281
|
-
<a href="dispatchAiFallback.mjs.html">dispatchAiFallback.mjs</a>, <a href="dispatchAiFallback.mjs.html#
|
|
2281
|
+
<a href="dispatchAiFallback.mjs.html">dispatchAiFallback.mjs</a>, <a href="dispatchAiFallback.mjs.html#line199">line 199</a>
|
|
2282
2282
|
</li></ul></dd>
|
|
2283
2283
|
|
|
2284
2284
|
|
|
@@ -2754,7 +2754,47 @@ await test()
|
|
|
2754
2754
|
</td>
|
|
2755
2755
|
|
|
2756
2756
|
|
|
2757
|
-
<td class="description last"><p>輸入狀態持久化物件{get:()=>state,set:(state)=>{}},state內含cursors(逐群組游標),省略代表用行程內記憶體(跨呼叫有效,重啟歸零)。假定單行程序列調用,並行請自行加鎖</p></td>
|
|
2757
|
+
<td class="description last"><p>輸入狀態持久化物件{get:()=>state,set:(state)=>{}},state內含cursors(逐群組游標)與cooling(供應商冷卻時間戳,僅cooldownMs>0時使用),省略代表用行程內記憶體(跨呼叫有效,重啟歸零)。假定單行程序列調用,並行請自行加鎖</p></td>
|
|
2758
|
+
</tr>
|
|
2759
|
+
|
|
2760
|
+
|
|
2761
|
+
|
|
2762
|
+
<tr>
|
|
2763
|
+
|
|
2764
|
+
<td class="name"><code>cooldownMs</code></td>
|
|
2765
|
+
|
|
2766
|
+
|
|
2767
|
+
<td class="type">
|
|
2768
|
+
|
|
2769
|
+
|
|
2770
|
+
<span class="param-type">Number</span>
|
|
2771
|
+
|
|
2772
|
+
|
|
2773
|
+
|
|
2774
|
+
|
|
2775
|
+
</td>
|
|
2776
|
+
|
|
2777
|
+
|
|
2778
|
+
<td class="attributes">
|
|
2779
|
+
|
|
2780
|
+
<optional><br>
|
|
2781
|
+
|
|
2782
|
+
|
|
2783
|
+
|
|
2784
|
+
|
|
2785
|
+
|
|
2786
|
+
</td>
|
|
2787
|
+
|
|
2788
|
+
|
|
2789
|
+
|
|
2790
|
+
<td class="default">
|
|
2791
|
+
|
|
2792
|
+
<code>0</code>
|
|
2793
|
+
|
|
2794
|
+
</td>
|
|
2795
|
+
|
|
2796
|
+
|
|
2797
|
+
<td class="description last"><p>輸入供應商冷卻視窗毫秒非負整數,>0啟用:條目(限有明給id者)遭遇限流(HTTP 429,僅api類可偵測)或逾時(TIMEOUT開頭)後,於冷卻視窗內之後續呼叫中被移至鏈尾——只降序不移除,前面全敗時仍會被嘗試,任一次成功立即解除;注意啟用時「providers順序即優先序」會被暫時重排,此即本機制之目的;預設0代表不啟用</p></td>
|
|
2758
2798
|
</tr>
|
|
2759
2799
|
|
|
2760
2800
|
|
|
@@ -7230,7 +7270,7 @@ console.log(getErrorResult(null).error)
|
|
|
7230
7270
|
|
|
7231
7271
|
<dt class="tag-source">Source:</dt>
|
|
7232
7272
|
<dd class="tag-source"><ul class="dummy"><li>
|
|
7233
|
-
<a href="dispatchAiFallback.mjs.html">dispatchAiFallback.mjs</a>, <a href="dispatchAiFallback.mjs.html#
|
|
7273
|
+
<a href="dispatchAiFallback.mjs.html">dispatchAiFallback.mjs</a>, <a href="dispatchAiFallback.mjs.html#line94">line 94</a>
|
|
7234
7274
|
</li></ul></dd>
|
|
7235
7275
|
|
|
7236
7276
|
|
|
@@ -8018,7 +8058,7 @@ await test()
|
|
|
8018
8058
|
</td>
|
|
8019
8059
|
|
|
8020
8060
|
|
|
8021
|
-
<td class="description last"><p>輸入前段名額規格陣列,各元素{ use, fallback, maxRetries?, timeoutMs? }等(
|
|
8061
|
+
<td class="description last"><p>輸入前段名額規格陣列,各元素{ use, fallback, check?, maxRetries?, timeoutMs? }等(check可覆寫頂層檢核;除use/fallback/check外之鍵覆寫該名額呼叫設定)</p></td>
|
|
8022
8062
|
</tr>
|
|
8023
8063
|
|
|
8024
8064
|
|
|
@@ -8054,7 +8094,7 @@ await test()
|
|
|
8054
8094
|
</td>
|
|
8055
8095
|
|
|
8056
8096
|
|
|
8057
|
-
<td class="description last"><p>輸入整合名額規格物件{ use, fallback, prompt?, ... },prompt可為(candidates)=>String
|
|
8097
|
+
<td class="description last"><p>輸入整合名額規格物件{ use, fallback, prompt?, check?, ... },prompt可為(candidates)=>String自訂整合提示詞(省略用預設模板);check為終稿專屬檢核(終稿判準常與候選不同,如須含固定段落),未給則沿用頂層check</p></td>
|
|
8058
8098
|
</tr>
|
|
8059
8099
|
|
|
8060
8100
|
|
|
@@ -8094,7 +8134,7 @@ await test()
|
|
|
8094
8134
|
</td>
|
|
8095
8135
|
|
|
8096
8136
|
|
|
8097
|
-
<td class="description last"><p
|
|
8137
|
+
<td class="description last"><p>輸入檢核函數(json)=>Boolean,作為候選與終稿之共用預設,名額規格與integrate可各自帶check覆寫,預設null</p></td>
|
|
8098
8138
|
</tr>
|
|
8099
8139
|
|
|
8100
8140
|
|
|
@@ -8679,7 +8719,7 @@ await test()
|
|
|
8679
8719
|
</td>
|
|
8680
8720
|
|
|
8681
8721
|
|
|
8682
|
-
<td class="description last"><p
|
|
8722
|
+
<td class="description last"><p>輸入前段共用檢核函數(前段名額規格與integrate可各自帶check覆寫,同runFanout),後段各階段自帶check,預設null</p></td>
|
|
8683
8723
|
</tr>
|
|
8684
8724
|
|
|
8685
8725
|
|
|
@@ -9270,7 +9310,7 @@ await test()
|
|
|
9270
9310
|
<br class="clear">
|
|
9271
9311
|
|
|
9272
9312
|
<footer>
|
|
9273
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026
|
|
9313
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026 15:11:06 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
9274
9314
|
</footer>
|
|
9275
9315
|
|
|
9276
9316
|
<script>prettyPrint();</script>
|
package/docs/index.html
CHANGED
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
<br class="clear">
|
|
72
72
|
|
|
73
73
|
<footer>
|
|
74
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026
|
|
74
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026 15:11:06 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
75
75
|
</footer>
|
|
76
76
|
|
|
77
77
|
<script>prettyPrint();</script>
|
|
@@ -176,7 +176,7 @@ export default resolveProviders
|
|
|
176
176
|
<br class="clear">
|
|
177
177
|
|
|
178
178
|
<footer>
|
|
179
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026
|
|
179
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026 15:11:06 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
180
180
|
</footer>
|
|
181
181
|
|
|
182
182
|
<script>prettyPrint();</script>
|
|
@@ -285,7 +285,7 @@ export { buildChain, NO_SIDE_EFFECT }
|
|
|
285
285
|
<br class="clear">
|
|
286
286
|
|
|
287
287
|
<footer>
|
|
288
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026
|
|
288
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026 15:11:06 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
289
289
|
</footer>
|
|
290
290
|
|
|
291
291
|
<script>prettyPrint();</script>
|
|
@@ -167,7 +167,7 @@ export default extractJsonLoose
|
|
|
167
167
|
<br class="clear">
|
|
168
168
|
|
|
169
169
|
<footer>
|
|
170
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026
|
|
170
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026 15:11:06 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
171
171
|
</footer>
|
|
172
172
|
|
|
173
173
|
<script>prettyPrint();</script>
|
|
@@ -97,9 +97,9 @@ ${candidates.map((c, i) => `【候選 ${i + 1}】\n${JSON.stringify(c)}`).join('
|
|
|
97
97
|
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
98
98
|
* @param {Object} opt.providers 輸入provider定義表物件(名稱 → 條目),透傳callAiWithFallback
|
|
99
99
|
* @param {String} opt.task 輸入前段各名額共用之任務提示詞字串
|
|
100
|
-
* @param {Array} opt.agents 輸入前段名額規格陣列,各元素{ use, fallback, maxRetries?, timeoutMs? }等(
|
|
101
|
-
* @param {Object} opt.integrate 輸入整合名額規格物件{ use, fallback, prompt?, ... },prompt可為(candidates)=>String
|
|
102
|
-
* @param {Function} [opt.check=null]
|
|
100
|
+
* @param {Array} opt.agents 輸入前段名額規格陣列,各元素{ use, fallback, check?, maxRetries?, timeoutMs? }等(check可覆寫頂層檢核;除use/fallback/check外之鍵覆寫該名額呼叫設定)
|
|
101
|
+
* @param {Object} opt.integrate 輸入整合名額規格物件{ use, fallback, prompt?, check?, ... },prompt可為(candidates)=>String自訂整合提示詞(省略用預設模板);check為終稿專屬檢核(終稿判準常與候選不同,如須含固定段落),未給則沿用頂層check
|
|
102
|
+
* @param {Function} [opt.check=null] 輸入檢核函數(json)=>Boolean,作為候選與終稿之共用預設,名額規格與integrate可各自帶check覆寫,預設null
|
|
103
103
|
* @param {String} [opt.schema=''] 輸入輸出格式示意字串,供預設整合模板嵌入,預設''
|
|
104
104
|
* @param {Number} [opt.minCandidates=2] 輸入進入整合所需之最少成功候選數正整數,未達門檻以首位候選為成果,預設2
|
|
105
105
|
* @param {Object} [opt.callOpt={}] 輸入透傳callAiWithFallback之共用設定(cwd、store、onEvent、timeoutMs、promptPrefix等),預設{}
|
|
@@ -161,9 +161,11 @@ async function runFanout(opt = {}) {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
//前段: 並行多開, 個別失敗不炸整輪
|
|
164
|
+
//各名額可自帶check覆寫頂層(候選與終稿判準本可不同); 自spec抽出而非留在overrides,
|
|
165
|
+
//否則會被後方明給之check靜默覆蓋(曾為死鍵)
|
|
164
166
|
let rsAgents = await Promise.all(agents.map((spec) => {
|
|
165
|
-
let { use, fallback, ...overrides } = spec
|
|
166
|
-
return callAiWithFallback(task, { ...callOpt, ...overrides, providers, spec: { use, fallback }, check })
|
|
167
|
+
let { use, fallback, check: checkAgent, ...overrides } = spec
|
|
168
|
+
return callAiWithFallback(task, { ...callOpt, ...overrides, providers, spec: { use, fallback }, check: isfun(checkAgent) ? checkAgent : check })
|
|
167
169
|
}))
|
|
168
170
|
let candidates = rsAgents.filter((r) => r.ok).map((r) => r.json)
|
|
169
171
|
|
|
@@ -181,9 +183,10 @@ async function runFanout(opt = {}) {
|
|
|
181
183
|
if (!integrate || !isestr(get(integrate, 'use', ''))) {
|
|
182
184
|
return { ok: false, result: null, integrated: false, agents: rsAgents, candidates, totalMs: Date.now() - t0, error: 'integrate spec (with use) is required' }
|
|
183
185
|
}
|
|
184
|
-
|
|
186
|
+
//整合可帶獨立check(終稿判準常較候選嚴, 如須含固定小標題), 未給則沿用頂層check
|
|
187
|
+
let { use, fallback, prompt: intPromptFn, check: checkInt, ...intOverrides } = integrate
|
|
185
188
|
let intPrompt = isfun(intPromptFn) ? intPromptFn(candidates) : defaultIntegratePrompt(candidates, opt)
|
|
186
|
-
let rInt = await callAiWithFallback(intPrompt, { ...callOpt, ...intOverrides, providers, spec: { use, fallback }, check })
|
|
189
|
+
let rInt = await callAiWithFallback(intPrompt, { ...callOpt, ...intOverrides, providers, spec: { use, fallback }, check: isfun(checkInt) ? checkInt : check })
|
|
187
190
|
|
|
188
191
|
return {
|
|
189
192
|
ok: rInt.ok,
|
|
@@ -214,7 +217,7 @@ export { defaultIntegratePrompt }
|
|
|
214
217
|
<br class="clear">
|
|
215
218
|
|
|
216
219
|
<footer>
|
|
217
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026
|
|
220
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026 15:11:06 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
218
221
|
</footer>
|
|
219
222
|
|
|
220
223
|
<script>prettyPrint();</script>
|
|
@@ -76,7 +76,7 @@ import runRolePipeline from './runRolePipeline.mjs'
|
|
|
76
76
|
* @param {Array} opt.agents 輸入前段名額規格陣列(同runFanout)
|
|
77
77
|
* @param {Object} opt.integrate 輸入前段整合名額規格物件(同runFanout)
|
|
78
78
|
* @param {Array} opt.stages 輸入後段階段規格陣列(同runRolePipeline),各階段以ctx.input取得前段成果
|
|
79
|
-
* @param {Function} [opt.check=null]
|
|
79
|
+
* @param {Function} [opt.check=null] 輸入前段共用檢核函數(前段名額規格與integrate可各自帶check覆寫,同runFanout),後段各階段自帶check,預設null
|
|
80
80
|
* @param {String} [opt.schema=''] 輸入輸出格式示意字串(供前段預設整合模板),預設''
|
|
81
81
|
* @param {Number} [opt.minCandidates=2] 輸入前段整合門檻正整數,預設2
|
|
82
82
|
* @param {Object} [opt.callOpt={}] 輸入透傳兩段之共用呼叫設定,預設{}
|
|
@@ -165,7 +165,7 @@ export default runFanoutPipeline
|
|
|
165
165
|
<br class="clear">
|
|
166
166
|
|
|
167
167
|
<footer>
|
|
168
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026
|
|
168
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026 15:11:06 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
169
169
|
</footer>
|
|
170
170
|
|
|
171
171
|
<script>prettyPrint();</script>
|
|
@@ -182,7 +182,7 @@ export default runRolePipeline
|
|
|
182
182
|
<br class="clear">
|
|
183
183
|
|
|
184
184
|
<footer>
|
|
185
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026
|
|
185
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Aug 14 2026 15:11:06 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
186
186
|
</footer>
|
|
187
187
|
|
|
188
188
|
<script>prettyPrint();</script>
|
package/package.json
CHANGED
|
@@ -27,6 +27,15 @@ import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
|
27
27
|
// 【跨次記憶只有游標】成功後游標推進至下一把, 令額度在同組多把金鑰間自動均攤;
|
|
28
28
|
// 死金鑰的代價也被游標攤平——這輪從key1敗轉key2成功後游標停在key3, 下輪不會先碰key1。
|
|
29
29
|
//
|
|
30
|
+
// 【供應商冷卻(選用, cooldownMs>0啟用, 預設關閉)】多階段工作流的每一階段都會從鏈首
|
|
31
|
+
// 重新探測同一家已失效的供應商——限流時每階段各踩一輪429、卡死時各燒一次完整逾時
|
|
32
|
+
// (使用端實測: 一次107秒的多階段請求中72秒耗在重複踩同一組429, 啟用冷卻後降至15秒)。
|
|
33
|
+
// 設計與「金鑰停用清單」(已否決)的關鍵差異: 以「條目」為單位、短視窗、且「只降序不移除」
|
|
34
|
+
// ——冷卻中的條目移到鏈尾而非移除, 前面全敗時照樣會被嘗試, 故不存在把已恢復服務冰住的問題;
|
|
35
|
+
// 任一次成功立即解除。觸發限於限流(HTTP 429, 僅api-openai-compat可靠偵測; CLI類之限流
|
|
36
|
+
// 埋在stderr文字中不納入)與逾時(TIMEOUT開頭, 各kind皆可)兩類——其餘失敗已有換金鑰換家
|
|
37
|
+
// 機制處理, 納入反而誤傷。狀態存於state.cooling, 與cursors同走store持久化。
|
|
38
|
+
//
|
|
30
39
|
// 【時間預算】budgetMs限制整輪遞補的總時長, 剩餘預算會壓進每次呼叫的timeoutMs,
|
|
31
40
|
// 防止多家連續卡逾時而撞破外部排程的執行上限。
|
|
32
41
|
//
|
|
@@ -60,7 +69,7 @@ import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
|
60
69
|
|
|
61
70
|
|
|
62
71
|
//fallback層自用之設定鍵, 其餘鍵作為各attempt之共用預設原樣轉傳
|
|
63
|
-
let FALLBACK_KEYS = ['providers', 'budgetMs', 'minAttemptMs', 'store', 'onEvent']
|
|
72
|
+
let FALLBACK_KEYS = ['providers', 'budgetMs', 'minAttemptMs', 'cooldownMs', 'store', 'onEvent']
|
|
64
73
|
|
|
65
74
|
|
|
66
75
|
//providers條目自用之設定鍵, 其餘鍵(含kind)即該條目之opt原樣轉傳對應轉接器
|
|
@@ -139,7 +148,8 @@ function isKeyIndependentFail(r) {
|
|
|
139
148
|
* @param {Array} [opt.providers[].keys=[]] 輸入同一服務之多把API key字串陣列,逐次注入輪替(kind為opencode時須同時於條目給予provider),省略代表沿用CLI既有登入狀態之單一虛擬金鑰
|
|
140
149
|
* @param {Number} [opt.budgetMs=null] 輸入整輪遞補之時間上限毫秒正整數,剩餘預算會壓進每次呼叫之timeoutMs,預設null代表不限
|
|
141
150
|
* @param {Number} [opt.minAttemptMs=20000] 輸入單次嘗試之最低剩餘預算毫秒正整數,剩餘低於此值即停止嘗試回報budget exhausted,預設20000
|
|
142
|
-
* @param {Object} [opt.store=null] 輸入狀態持久化物件{get:()=>state,set:(state)=>{}},state內含cursors(逐群組游標),省略代表用行程內記憶體(跨呼叫有效,重啟歸零)。假定單行程序列調用,並行請自行加鎖
|
|
151
|
+
* @param {Object} [opt.store=null] 輸入狀態持久化物件{get:()=>state,set:(state)=>{}},state內含cursors(逐群組游標)與cooling(供應商冷卻時間戳,僅cooldownMs>0時使用),省略代表用行程內記憶體(跨呼叫有效,重啟歸零)。假定單行程序列調用,並行請自行加鎖
|
|
152
|
+
* @param {Number} [opt.cooldownMs=0] 輸入供應商冷卻視窗毫秒非負整數,>0啟用:條目(限有明給id者)遭遇限流(HTTP 429,僅api類可偵測)或逾時(TIMEOUT開頭)後,於冷卻視窗內之後續呼叫中被移至鏈尾——只降序不移除,前面全敗時仍會被嘗試,任一次成功立即解除;注意啟用時「providers順序即優先序」會被暫時重排,此即本機制之目的;預設0代表不啟用
|
|
143
153
|
* @param {Function} [opt.onEvent=null] 輸入事件回調函數(ev)=>{},ev.type可為'try'、'ok'、'next-key'、'skip-group'、'budget-out';失敗事件(next-key/skip-group)另帶stdout(被拒回覆)與stderr(錯誤輸出)供診斷,兩者於失敗路徑已由轉接器截斷;回調拋出例外不影響主流程,預設null
|
|
144
154
|
* @param {Number} [opt.timeoutMs=300000] 輸入各attempt共用之逾時毫秒正整數,條目可覆寫,全套件統一預設300000
|
|
145
155
|
* @param {String|Function} [opt.validate=undefined] 輸入各attempt共用之stdout驗證規則,條目可覆寫,預設undefined
|
|
@@ -235,6 +245,9 @@ async function dispatchAiFallback(prompt, opt = {}) {
|
|
|
235
245
|
if (!isobj(state.cursors)) {
|
|
236
246
|
state.cursors = {}
|
|
237
247
|
}
|
|
248
|
+
if (!isobj(state.cooling)) {
|
|
249
|
+
state.cooling = {}
|
|
250
|
+
}
|
|
238
251
|
let saveState = () => {
|
|
239
252
|
if (useStore) {
|
|
240
253
|
try {
|
|
@@ -244,6 +257,43 @@ async function dispatchAiFallback(prompt, opt = {}) {
|
|
|
244
257
|
}
|
|
245
258
|
}
|
|
246
259
|
|
|
260
|
+
//cooldownMs, 無效視為0=不啟用(現行行為零改變)
|
|
261
|
+
let cooldownMs = get(opt, 'cooldownMs', null)
|
|
262
|
+
if (!ispint(cooldownMs)) {
|
|
263
|
+
cooldownMs = 0
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
cooldownMs = cint(cooldownMs)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
//供應商冷卻: 冷卻中的條目「只降序不移除」——移到鏈尾, 前面全敗時仍會被嘗試,
|
|
270
|
+
//故不存在把已恢復服務冰住的問題(此為與「金鑰停用清單」的關鍵差異, 後者已被否決)。
|
|
271
|
+
//僅追蹤有明給id之條目(索引式id會因重排而錯位); 過期項順手清除
|
|
272
|
+
if (cooldownMs > 0) {
|
|
273
|
+
let now = Date.now()
|
|
274
|
+
let act = []
|
|
275
|
+
let cool = []
|
|
276
|
+
let dirty = false
|
|
277
|
+
for (let p of providers) {
|
|
278
|
+
let pid = get(p, 'id', null)
|
|
279
|
+
let ts = isestr(pid) ? get(state.cooling, pid, null) : null
|
|
280
|
+
if (ispint(ts) && (now - ts) < cooldownMs) {
|
|
281
|
+
cool.push(p)
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
if (isestr(pid) && state.cooling[pid] !== undefined) {
|
|
285
|
+
delete state.cooling[pid] //冷卻已過期, 清除
|
|
286
|
+
dirty = true
|
|
287
|
+
}
|
|
288
|
+
act.push(p)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
providers = [...act, ...cool]
|
|
292
|
+
if (dirty) {
|
|
293
|
+
saveState()
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
247
297
|
//onEvent, 回調拋出例外不得中斷主流程
|
|
248
298
|
let onEvent = get(opt, 'onEvent', null)
|
|
249
299
|
let emit = (ev) => {
|
|
@@ -266,9 +316,10 @@ async function dispatchAiFallback(prompt, opt = {}) {
|
|
|
266
316
|
for (let ig = 0; ig < providers.length; ig++) {
|
|
267
317
|
let entry = providers[ig]
|
|
268
318
|
|
|
269
|
-
//id,
|
|
319
|
+
//id, 無效回退條目索引字串; idExplicit供冷卻機制判別(索引式id不參與冷卻)
|
|
270
320
|
let id = get(entry, 'id', null)
|
|
271
|
-
|
|
321
|
+
let idExplicit = isestr(id)
|
|
322
|
+
if (!idExplicit) {
|
|
272
323
|
id = String(ig)
|
|
273
324
|
}
|
|
274
325
|
|
|
@@ -331,8 +382,12 @@ async function dispatchAiFallback(prompt, opt = {}) {
|
|
|
331
382
|
emit({ type: 'try', providerId: id, keyIndex, keyId, kind, model })
|
|
332
383
|
let r = await dispatchAi(kind, prompt, attemptOpt)
|
|
333
384
|
|
|
334
|
-
//成功, 推進游標(額度均攤)
|
|
385
|
+
//成功, 推進游標(額度均攤)並回傳; 任一次成功立即解除該家冷卻
|
|
335
386
|
if (r.ok) {
|
|
387
|
+
if (cooldownMs > 0 && idExplicit && state.cooling[id] !== undefined) {
|
|
388
|
+
delete state.cooling[id]
|
|
389
|
+
saveState()
|
|
390
|
+
}
|
|
336
391
|
if (nk > 0) {
|
|
337
392
|
state.cursors[id] = (keyIndex + 1) % nk
|
|
338
393
|
saveState()
|
|
@@ -345,6 +400,16 @@ async function dispatchAiFallback(prompt, opt = {}) {
|
|
|
345
400
|
//失敗分流
|
|
346
401
|
lastResult = r
|
|
347
402
|
lastMeta = { providerId: id, keyIndex, kind, model }
|
|
403
|
+
//冷卻觸發: 限流(HTTP 429, 僅api類可偵測)與逾時(TIMEOUT開頭, CLI與api皆可)兩類——
|
|
404
|
+
//其餘失敗(金鑰無效/服務端錯誤)已有換金鑰換家機制處理, 納入冷卻反而誤傷
|
|
405
|
+
if (cooldownMs > 0 && idExplicit) {
|
|
406
|
+
let isCoolTrigger = (r.code === 429) || (isestr(r.error) && r.error.indexOf('TIMEOUT') === 0)
|
|
407
|
+
if (isCoolTrigger) {
|
|
408
|
+
state.cooling[id] = Date.now()
|
|
409
|
+
saveState()
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
348
413
|
//失敗事件與tried一併帶被拒回覆(stdout)與錯誤輸出(stderr), 供呼叫端診斷失敗原因
|
|
349
414
|
//(如驗證失敗時模型究竟回了什麼); 兩者於失敗路徑已由轉接器截斷(≤500/1000字元), 不會過大
|
|
350
415
|
if (isKeyIndependentFail(r)) {
|
package/src/wkf/runFanout.mjs
CHANGED
|
@@ -50,9 +50,9 @@ ${candidates.map((c, i) => `【候選 ${i + 1}】\n${JSON.stringify(c)}`).join('
|
|
|
50
50
|
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
51
51
|
* @param {Object} opt.providers 輸入provider定義表物件(名稱 → 條目),透傳callAiWithFallback
|
|
52
52
|
* @param {String} opt.task 輸入前段各名額共用之任務提示詞字串
|
|
53
|
-
* @param {Array} opt.agents 輸入前段名額規格陣列,各元素{ use, fallback, maxRetries?, timeoutMs? }等(
|
|
54
|
-
* @param {Object} opt.integrate 輸入整合名額規格物件{ use, fallback, prompt?, ... },prompt可為(candidates)=>String
|
|
55
|
-
* @param {Function} [opt.check=null]
|
|
53
|
+
* @param {Array} opt.agents 輸入前段名額規格陣列,各元素{ use, fallback, check?, maxRetries?, timeoutMs? }等(check可覆寫頂層檢核;除use/fallback/check外之鍵覆寫該名額呼叫設定)
|
|
54
|
+
* @param {Object} opt.integrate 輸入整合名額規格物件{ use, fallback, prompt?, check?, ... },prompt可為(candidates)=>String自訂整合提示詞(省略用預設模板);check為終稿專屬檢核(終稿判準常與候選不同,如須含固定段落),未給則沿用頂層check
|
|
55
|
+
* @param {Function} [opt.check=null] 輸入檢核函數(json)=>Boolean,作為候選與終稿之共用預設,名額規格與integrate可各自帶check覆寫,預設null
|
|
56
56
|
* @param {String} [opt.schema=''] 輸入輸出格式示意字串,供預設整合模板嵌入,預設''
|
|
57
57
|
* @param {Number} [opt.minCandidates=2] 輸入進入整合所需之最少成功候選數正整數,未達門檻以首位候選為成果,預設2
|
|
58
58
|
* @param {Object} [opt.callOpt={}] 輸入透傳callAiWithFallback之共用設定(cwd、store、onEvent、timeoutMs、promptPrefix等),預設{}
|
|
@@ -114,9 +114,11 @@ async function runFanout(opt = {}) {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
//前段: 並行多開, 個別失敗不炸整輪
|
|
117
|
+
//各名額可自帶check覆寫頂層(候選與終稿判準本可不同); 自spec抽出而非留在overrides,
|
|
118
|
+
//否則會被後方明給之check靜默覆蓋(曾為死鍵)
|
|
117
119
|
let rsAgents = await Promise.all(agents.map((spec) => {
|
|
118
|
-
let { use, fallback, ...overrides } = spec
|
|
119
|
-
return callAiWithFallback(task, { ...callOpt, ...overrides, providers, spec: { use, fallback }, check })
|
|
120
|
+
let { use, fallback, check: checkAgent, ...overrides } = spec
|
|
121
|
+
return callAiWithFallback(task, { ...callOpt, ...overrides, providers, spec: { use, fallback }, check: isfun(checkAgent) ? checkAgent : check })
|
|
120
122
|
}))
|
|
121
123
|
let candidates = rsAgents.filter((r) => r.ok).map((r) => r.json)
|
|
122
124
|
|
|
@@ -134,9 +136,10 @@ async function runFanout(opt = {}) {
|
|
|
134
136
|
if (!integrate || !isestr(get(integrate, 'use', ''))) {
|
|
135
137
|
return { ok: false, result: null, integrated: false, agents: rsAgents, candidates, totalMs: Date.now() - t0, error: 'integrate spec (with use) is required' }
|
|
136
138
|
}
|
|
137
|
-
|
|
139
|
+
//整合可帶獨立check(終稿判準常較候選嚴, 如須含固定小標題), 未給則沿用頂層check
|
|
140
|
+
let { use, fallback, prompt: intPromptFn, check: checkInt, ...intOverrides } = integrate
|
|
138
141
|
let intPrompt = isfun(intPromptFn) ? intPromptFn(candidates) : defaultIntegratePrompt(candidates, opt)
|
|
139
|
-
let rInt = await callAiWithFallback(intPrompt, { ...callOpt, ...intOverrides, providers, spec: { use, fallback }, check })
|
|
142
|
+
let rInt = await callAiWithFallback(intPrompt, { ...callOpt, ...intOverrides, providers, spec: { use, fallback }, check: isfun(checkInt) ? checkInt : check })
|
|
140
143
|
|
|
141
144
|
return {
|
|
142
145
|
ok: rInt.ok,
|
|
@@ -29,7 +29,7 @@ import runRolePipeline from './runRolePipeline.mjs'
|
|
|
29
29
|
* @param {Array} opt.agents 輸入前段名額規格陣列(同runFanout)
|
|
30
30
|
* @param {Object} opt.integrate 輸入前段整合名額規格物件(同runFanout)
|
|
31
31
|
* @param {Array} opt.stages 輸入後段階段規格陣列(同runRolePipeline),各階段以ctx.input取得前段成果
|
|
32
|
-
* @param {Function} [opt.check=null]
|
|
32
|
+
* @param {Function} [opt.check=null] 輸入前段共用檢核函數(前段名額規格與integrate可各自帶check覆寫,同runFanout),後段各階段自帶check,預設null
|
|
33
33
|
* @param {String} [opt.schema=''] 輸入輸出格式示意字串(供前段預設整合模板),預設''
|
|
34
34
|
* @param {Number} [opt.minCandidates=2] 輸入前段整合門檻正整數,預設2
|
|
35
35
|
* @param {Object} [opt.callOpt={}] 輸入透傳兩段之共用呼叫設定,預設{}
|
|
@@ -316,6 +316,101 @@ describe('dispatchAiFallback', function() {
|
|
|
316
316
|
assert.strict.deepEqual(r, rr)
|
|
317
317
|
})
|
|
318
318
|
|
|
319
|
+
it('cooldownMs啟用時, 逾時觸發冷卻且次輪該條目降至鏈尾(只降序不移除)', async function() {
|
|
320
|
+
let stored = { cursors: {}, cooling: {} }
|
|
321
|
+
let store = {
|
|
322
|
+
get: () => stored,
|
|
323
|
+
set: (s) => {
|
|
324
|
+
stored = s
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
let pHang = { id: 'cd-hang', kind: 'claude', exe: fake.exe, extraArgs: ['--fake-sleep=9000'], timeoutMs: 500 }
|
|
328
|
+
let pOk = { id: 'cd-ok', kind: 'claude', exe: fake.exe }
|
|
329
|
+
//第1輪: hang在鏈首 → 逾時跳組並觸發冷卻 → ok遞補
|
|
330
|
+
let r1 = await dispatchAiFallback('abc', { providers: [pHang, pOk], store, cooldownMs: 300000 })
|
|
331
|
+
//第2輪: hang冷卻中 → 降至鏈尾, ok先上且成功, hang完全未被嘗試
|
|
332
|
+
let r2 = await dispatchAiFallback('abc', { providers: [pHang, pOk], store, cooldownMs: 300000 })
|
|
333
|
+
let r = [
|
|
334
|
+
r1.providerId,
|
|
335
|
+
r1.tried.map((x) => x.providerId),
|
|
336
|
+
typeof stored.cooling['cd-hang'],
|
|
337
|
+
r2.providerId,
|
|
338
|
+
r2.tried.map((x) => x.providerId), //只有cd-ok, 未浪費一次逾時
|
|
339
|
+
]
|
|
340
|
+
let rr = ['cd-ok', ['cd-hang', 'cd-ok'], 'number', 'cd-ok', ['cd-ok']]
|
|
341
|
+
assert.strict.deepEqual(r, rr)
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('冷卻中的條目於前面全敗時仍會被嘗試, 且成功即解除冷卻', async function() {
|
|
345
|
+
let stored = { cursors: {}, cooling: { 'cd2-a': Date.now() } } //預置: a冷卻中
|
|
346
|
+
let store = {
|
|
347
|
+
get: () => stored,
|
|
348
|
+
set: (s) => {
|
|
349
|
+
stored = s
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
let r1 = await dispatchAiFallback('abc', {
|
|
353
|
+
providers: [
|
|
354
|
+
{ id: 'cd2-a', kind: 'claude', exe: fake.exe }, //冷卻中 → 降至鏈尾
|
|
355
|
+
{ id: 'cd2-b', kind: 'claude', exe: fake.exe, extraArgs: ['--fake-exit=1'] }, //鏈首但必敗
|
|
356
|
+
],
|
|
357
|
+
store,
|
|
358
|
+
cooldownMs: 300000,
|
|
359
|
+
})
|
|
360
|
+
let r = [
|
|
361
|
+
r1.ok,
|
|
362
|
+
r1.providerId, //b先上失敗 → 降序後的a仍被嘗試且成功
|
|
363
|
+
r1.tried.map((x) => x.providerId),
|
|
364
|
+
stored.cooling['cd2-a'], //成功即解除
|
|
365
|
+
]
|
|
366
|
+
let rr = [true, 'cd2-a', ['cd2-b', 'cd2-a'], undefined]
|
|
367
|
+
assert.strict.deepEqual(r, rr)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
it('冷卻過期後恢復原順序並清除過期紀錄', async function() {
|
|
371
|
+
let stored = { cursors: {}, cooling: { 'cd3-a': Date.now() - 1000 } } //1秒前觸發
|
|
372
|
+
let store = {
|
|
373
|
+
get: () => stored,
|
|
374
|
+
set: (s) => {
|
|
375
|
+
stored = s
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
let r1 = await dispatchAiFallback('abc', {
|
|
379
|
+
providers: [
|
|
380
|
+
{ id: 'cd3-a', kind: 'claude', exe: fake.exe },
|
|
381
|
+
{ id: 'cd3-b', kind: 'claude', exe: fake.exe },
|
|
382
|
+
],
|
|
383
|
+
store,
|
|
384
|
+
cooldownMs: 500, //視窗0.5秒, 已過期
|
|
385
|
+
})
|
|
386
|
+
let r = [r1.providerId, stored.cooling['cd3-a']]
|
|
387
|
+
let rr = ['cd3-a', undefined] //恢復鏈首, 過期紀錄被清除
|
|
388
|
+
assert.strict.deepEqual(r, rr)
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('cooldownMs未啟用(預設)時不重排也不寫入cooling', async function() {
|
|
392
|
+
let stored = { cursors: {}, cooling: { 'cd4-a': Date.now() } }
|
|
393
|
+
let store = {
|
|
394
|
+
get: () => stored,
|
|
395
|
+
set: (s) => {
|
|
396
|
+
stored = s
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
let r1 = await dispatchAiFallback('abc', {
|
|
400
|
+
providers: [
|
|
401
|
+
{ id: 'cd4-a', kind: 'claude', exe: fake.exe, extraArgs: ['--fake-sleep=9000'], timeoutMs: 500 },
|
|
402
|
+
{ id: 'cd4-b', kind: 'claude', exe: fake.exe },
|
|
403
|
+
],
|
|
404
|
+
store,
|
|
405
|
+
})
|
|
406
|
+
let r = [
|
|
407
|
+
r1.tried.map((x) => x.providerId), //cooling有預置紀錄仍不重排(未啟用)
|
|
408
|
+
typeof stored.cooling['cd4-a'] === 'number' && stored.cooling['cd4-b'] === undefined, //逾時亦不新增紀錄
|
|
409
|
+
]
|
|
410
|
+
let rr = [['cd4-a', 'cd4-b'], true]
|
|
411
|
+
assert.strict.deepEqual(r, rr)
|
|
412
|
+
})
|
|
413
|
+
|
|
319
414
|
it('失敗事件與tried帶被拒回覆(stdout)與錯誤輸出(stderr)供診斷', async function() {
|
|
320
415
|
let evs = []
|
|
321
416
|
let t = await dispatchAiFallback('abc', {
|
|
@@ -245,6 +245,34 @@ describe('dispatchApiOpenaiCompat', function() {
|
|
|
245
245
|
assert.strict.deepEqual(r, rr)
|
|
246
246
|
})
|
|
247
247
|
|
|
248
|
+
it('HTTP 429觸發冷卻: 次輪該條目降至鏈尾', async function() {
|
|
249
|
+
let stored = { cursors: {}, cooling: {} }
|
|
250
|
+
let store = {
|
|
251
|
+
get: () => stored,
|
|
252
|
+
set: (s) => {
|
|
253
|
+
stored = s
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
//flaky-429對同一金鑰首次回429; maxRetries預設0故第1輪即失敗並觸發冷卻
|
|
257
|
+
let p429 = { id: 'cd-api-429', kind: 'api-openai-compat', baseURL: svr.url, model: 'flaky-429', keys: ['sk-good-cd429'] }
|
|
258
|
+
let pEcho = { id: 'cd-api-echo', kind: 'api-openai-compat', baseURL: svr.url, model: 'echo', keys: ['sk-good-cd-e'] }
|
|
259
|
+
let r1 = await dispatchAiFallback('abc', { providers: [p429, pEcho], store, cooldownMs: 300000 })
|
|
260
|
+
let r2 = await dispatchAiFallback('abc', { providers: [p429, pEcho], store, cooldownMs: 300000 })
|
|
261
|
+
let r = [
|
|
262
|
+
r1.providerId,
|
|
263
|
+
r1.tried.map((x) => [x.providerId, x.error || null]),
|
|
264
|
+
typeof stored.cooling['cd-api-429'],
|
|
265
|
+
r2.tried.map((x) => x.providerId), //429者已降尾, echo先上即成功
|
|
266
|
+
]
|
|
267
|
+
let rr = [
|
|
268
|
+
'cd-api-echo',
|
|
269
|
+
[['cd-api-429', 'HTTP 429'], ['cd-api-echo', null]],
|
|
270
|
+
'number',
|
|
271
|
+
['cd-api-echo'],
|
|
272
|
+
]
|
|
273
|
+
assert.strict.deepEqual(r, rr)
|
|
274
|
+
})
|
|
275
|
+
|
|
248
276
|
it('可作為工作流provider, 回覆JSON經解析與檢核', async function() {
|
|
249
277
|
let wkf = dispatchAiWkf({
|
|
250
278
|
providers: {
|