w-dispatch-ai 1.0.2 → 1.0.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.
Files changed (45) hide show
  1. package/README.md +123 -35
  2. package/dist/w-dispatch-ai.umd.js +2 -2
  3. package/dist/w-dispatch-ai.umd.js.map +1 -1
  4. package/docs/WDispatchAi.mjs.html +8 -4
  5. package/docs/adapters.mjs.html +43 -7
  6. package/docs/dispatchAi.mjs.html +2 -2
  7. package/docs/dispatchAiFallback.mjs.html +42 -8
  8. package/docs/dispatchAiWkf.mjs.html +197 -0
  9. package/docs/dispatchAntigravity.mjs.html +2 -2
  10. package/docs/dispatchApiOpenaiCompat.mjs.html +516 -0
  11. package/docs/dispatchClaude.mjs.html +2 -2
  12. package/docs/dispatchCodex.mjs.html +2 -2
  13. package/docs/dispatchOpencode.mjs.html +2 -2
  14. package/docs/getCliArgs.mjs.html +2 -2
  15. package/docs/getErrorResult.mjs.html +2 -2
  16. package/docs/global.html +5768 -1469
  17. package/docs/index.html +2 -2
  18. package/docs/wkf_callAiWithFallback.mjs.html +282 -0
  19. package/docs/wkf_extractJsonLoose.mjs.html +180 -0
  20. package/docs/wkf_runFanout.mjs.html +227 -0
  21. package/docs/wkf_runFanoutPipeline.mjs.html +178 -0
  22. package/docs/wkf_runRolePipeline.mjs.html +195 -0
  23. package/g.mjs +41 -25
  24. package/package.json +1 -1
  25. package/src/WDispatchAi.mjs +6 -2
  26. package/src/adapters.mjs +41 -5
  27. package/src/dispatchAiFallback.mjs +40 -6
  28. package/src/dispatchAiWkf.mjs +125 -0
  29. package/src/dispatchApiOpenaiCompat.mjs +444 -0
  30. package/src/wkf/callAiWithFallback.mjs +210 -0
  31. package/src/wkf/extractJsonLoose.mjs +108 -0
  32. package/src/wkf/runFanout.mjs +155 -0
  33. package/src/wkf/runFanoutPipeline.mjs +106 -0
  34. package/src/wkf/runRolePipeline.mjs +123 -0
  35. package/test/tools/fakeServerForApiTest.mjs +151 -0
  36. package/test/unit-WDispatchAi.test.mjs +13 -6
  37. package/test/unit-adapters.test.mjs +5 -3
  38. package/test/unit-callAiWithFallback.test.mjs +146 -0
  39. package/test/unit-dispatchAi.test.mjs +1 -1
  40. package/test/unit-dispatchAiWkf.test.mjs +118 -0
  41. package/test/unit-dispatchApiOpenaiCompat.test.mjs +264 -0
  42. package/test/unit-extractJsonLoose.test.mjs +78 -0
  43. package/test/unit-runFanout.test.mjs +166 -0
  44. package/test/unit-runFanoutPipeline.test.mjs +86 -0
  45. package/test/unit-runRolePipeline.test.mjs +163 -0
@@ -0,0 +1,516 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>dispatchApiOpenaiCompat.mjs - Documentation</title>
7
+
8
+
9
+ <script src="scripts/prettify/prettify.js"></script>
10
+ <script src="scripts/prettify/lang-css.js"></script>
11
+ <!--[if lt IE 9]>
12
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
+ <![endif]-->
14
+ <link type="text/css" rel="stylesheet" href="styles/prettify.css">
15
+ <link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
16
+ <script src="scripts/nav.js" defer></script>
17
+
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ </head>
20
+ <body>
21
+
22
+ <input type="checkbox" id="nav-trigger" class="nav-trigger" />
23
+ <label for="nav-trigger" class="navicon-button x">
24
+ <div class="navicon"></div>
25
+ </label>
26
+
27
+ <label for="nav-trigger" class="overlay"></label>
28
+
29
+ <nav >
30
+
31
+
32
+ <h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#buildChain">buildChain</a></li><li><a href="global.html#buildValidator">buildValidator</a></li><li><a href="global.html#callAiWithFallback">callAiWithFallback</a></li><li><a href="global.html#callOnce">callOnce</a></li><li><a href="global.html#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dispatchAi">dispatchAi</a></li><li><a href="global.html#dispatchAiFallback">dispatchAiFallback</a></li><li><a href="global.html#dispatchAiWkf">dispatchAiWkf</a></li><li><a href="global.html#dispatchAntigravity">dispatchAntigravity</a></li><li><a href="global.html#dispatchApiOpenaiCompat">dispatchApiOpenaiCompat</a></li><li><a href="global.html#dispatchClaude">dispatchClaude</a></li><li><a href="global.html#dispatchCodex">dispatchCodex</a></li><li><a href="global.html#dispatchOpencode">dispatchOpencode</a></li><li><a href="global.html#extractJsonLoose">extractJsonLoose</a></li><li><a href="global.html#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li><li><a href="global.html#runFanout">runFanout</a></li><li><a href="global.html#runFanoutPipeline">runFanoutPipeline</a></li><li><a href="global.html#runRolePipeline">runRolePipeline</a></li></ul>
33
+
34
+ </nav>
35
+
36
+ <div id="main">
37
+
38
+ <h1 class="page-title">dispatchApiOpenaiCompat.mjs</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>import get from 'lodash-es/get.js'
49
+ import isobj from 'wsemi/src/isobj.mjs'
50
+ import isfun from 'wsemi/src/isfun.mjs'
51
+ import isnum from 'wsemi/src/isnum.mjs'
52
+ import isestr from 'wsemi/src/isestr.mjs'
53
+ import ispint from 'wsemi/src/ispint.mjs'
54
+ import isp0int from 'wsemi/src/isp0int.mjs'
55
+ import cint from 'wsemi/src/cint.mjs'
56
+ import delay from 'wsemi/src/delay.mjs'
57
+ import strleft from 'wsemi/src/strleft.mjs'
58
+ import strdelleft from 'wsemi/src/strdelleft.mjs'
59
+ import strTruncate from 'wsemi/src/strTruncate.mjs'
60
+ import getErrorResult from './getErrorResult.mjs'
61
+
62
+
63
+ // dispatchApiOpenaiCompat.mjs — 以fetch直呼OpenAI相容API(chat/completions)
64
+ //
65
+ // 【為何需要】opencode CLI調用的deepseek(OpenCode Zen閘道)與agnes-ai本體都是
66
+ // OpenAI相容REST API, 直呼即可免安裝CLI、免預先登入(2026-08-11於本機實測):
67
+ // OpenCode Zen — https://opencode.ai/zen/v1 (金鑰同auth.json之sk-..., 模型名去掉opencode/前綴)
68
+ // Agnes — https://apihub.agnes-ai.com/v1
69
+ // 實測四把金鑰直呼皆200; 壞金鑰回401(Zen: `{"type":"AuthError","message":"Invalid API key."}`,
70
+ // Agnes: `{"message":"无效的令牌...","type":"AgnesAI_error"}`)。
71
+ //
72
+ // 【與CLI轉接器之差異】prompt走HTTP body無命令列長度限制; 錯誤依HTTP狀態碼精確分流
73
+ // (401/403金鑰、429限流、5xx服務端), 不再依賴stderr字串猜測; 純completion無agentic
74
+ // 能力(不讀檔不跑指令), 天然無寫檔風險。
75
+ // 注意: claude與codex走訂閱帳號登入態而非API金鑰, 無法比照, 仍須CLI轉接器。
76
+ //
77
+ // 【本轉接器不支援工具, 需要工具請改用CLI類kind(2026-08-11實測後之決策)】
78
+ // 閘道端零內建工具: 實測Zen與Agnes皆對`tools:[{type:'web_search'}]`回400並要求
79
+ // function.parameters, 即只接受「呼叫端自行定義且自行執行」之function工具;
80
+ // 協定層雖支援function calling(Zen之nemotron-3-ultra-free與Agnes皆實測回
81
+ // finish_reason:'tool_calls'且tool_calls格式標準), 但工具之定義、執行、錯誤處理與
82
+ // 安全邊界全須本套件自行實作與維護, 等同重造CLI已提供之harness, 故不做。
83
+ // 又tool_calls有會話束縛(tool_call_id須於同一條messages串內回填, 且須保留前文),
84
+ // 該messages串活在本函數單次呼叫之生命週期內, 無法暫停後跨行程外傳給上層agent代跑
85
+ // ——工作流是被上層阻塞呼叫的函式, 沒有反向請求工具的通道; 同理工作流(如runFanout)
86
+ // 之各名額亦只是同行程之async函數呼叫, 無法把tool_calls往上層轉送。
87
+ // 故呼叫端若於body帶入tools, 本函數一律以TOOL_CALLS_UNSUPPORTED回報失敗而不假裝成功
88
+ // (實測Agnes於tool_calls時content為"\n\n"而非null, 不特別處理會靜默回傳空白內容)。
89
+ //
90
+ // 【重試語意對齊execCli】4xx(429除外)為客戶端錯誤不可重試而立即中止;
91
+ // 429/5xx/網路錯誤/逾時依maxRetries線性退避重試(間隔retryDelayMs*次數, 上限15000ms)。
92
+ //
93
+ // 【結果結構對齊execCli】{ ok, stdout, stderr, code, error, durationMs, attempts },
94
+ // stdout為回覆內容、code為HTTP狀態碼(網路錯誤與逾時為null)、逾時error以TIMEOUT開頭、
95
+ // 驗證失敗error為OUTPUT_VALIDATION_FAILED——故dispatchAiFallback之失敗分流
96
+ // (TIMEOUT/驗證失敗跳組, 其餘換金鑰)對本轉接器同樣成立, 無須任何修改。
97
+
98
+
99
+ //預設值
100
+ let DEFAULT_TIMEOUT_MS = 120000
101
+ let DEFAULT_RETRY_DELAY_MS = 5000
102
+ let MAX_RETRY_DELAY_MS = 15000
103
+
104
+
105
+ //optTruncate, 裁切失敗結果之內容時於刪節號後標註原始總長度(同execCli)
106
+ let optTruncate = {
107
+ funWithMsg: (str) => `(truncated, total ${str.length} chars)`,
108
+ }
109
+
110
+
111
+ /**
112
+ * 建立驗證函式(規則語法同execCli之validate)
113
+ *
114
+ * @param {String|Function} rule 輸入驗證規則字串('nonempty'、'json'、'min:100', 逗號可串接)或自訂函式
115
+ * @returns {Function|null} 回傳驗證函式,無有效規則回傳null
116
+ */
117
+ function buildValidator(rule) {
118
+
119
+ //自訂函式直接使用
120
+ if (isfun(rule)) {
121
+ return rule
122
+ }
123
+
124
+ //check
125
+ if (!isestr(rule)) {
126
+ return null
127
+ }
128
+
129
+ //checks
130
+ let checks = rule.split(',').map((r) => r.trim()).filter(Boolean)
131
+ if (checks.length === 0) {
132
+ return null
133
+ }
134
+
135
+ return (stdout) => {
136
+ for (let check of checks) {
137
+
138
+ if (check === 'nonempty') {
139
+ if (!isestr(stdout) || stdout.trim() === '') {
140
+ return false
141
+ }
142
+ }
143
+
144
+ else if (check === 'json') {
145
+ try {
146
+ JSON.parse(stdout)
147
+ }
148
+ catch {
149
+ return false
150
+ }
151
+ }
152
+
153
+ else if (strleft(check, 4) === 'min:') {
154
+
155
+ //規則本身無效(如min:abc) → 視為驗證失敗, 不靜默跳過
156
+ let smin = strdelleft(check, 4)
157
+ if (!isnum(smin)) {
158
+ return false
159
+ }
160
+
161
+ let min = cint(smin)
162
+ if (!isestr(stdout) || stdout.length &lt; min) {
163
+ return false
164
+ }
165
+ }
166
+
167
+ }
168
+ return true
169
+ }
170
+ }
171
+
172
+
173
+ /**
174
+ * 單次HTTP呼叫(內部使用, 不含重試邏輯)
175
+ *
176
+ * @param {String} url 輸入完整端點網址字串
177
+ * @param {Object} headers 輸入請求標頭物件
178
+ * @param {Object} body 輸入請求本體物件
179
+ * @param {Number} timeoutMs 輸入逾時毫秒
180
+ * @param {Function|null} validator 輸入驗證函式
181
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件
182
+ */
183
+ async function callOnce(url, headers, body, timeoutMs, validator) {
184
+
185
+ let t0 = Date.now()
186
+
187
+ //AbortController, 逾時中止(含回應本體之串流讀取)
188
+ let controller = new AbortController()
189
+ let timer = setTimeout(() => {
190
+ controller.abort()
191
+ }, timeoutMs)
192
+
193
+ let res = null
194
+ let txt = ''
195
+ try {
196
+ res = await fetch(url, {
197
+ method: 'POST',
198
+ headers,
199
+ body: JSON.stringify(body),
200
+ signal: controller.signal,
201
+ })
202
+ txt = await res.text()
203
+ }
204
+ catch (err) {
205
+ clearTimeout(timer)
206
+ let durationMs = Date.now() - t0
207
+
208
+ //逾時, error以TIMEOUT開頭令dispatchAiFallback視為與金鑰無關而跳組
209
+ if (err.name === 'AbortError') {
210
+ return {
211
+ ok: false,
212
+ stdout: '',
213
+ stderr: '',
214
+ code: null,
215
+ error: `TIMEOUT after ${timeoutMs / 1000}s`,
216
+ durationMs,
217
+ }
218
+ }
219
+
220
+ //網路層錯誤(DNS/連線拒絕等)
221
+ let cause = get(err, 'cause.code', '') || err.message
222
+ return {
223
+ ok: false,
224
+ stdout: '',
225
+ stderr: '',
226
+ code: null,
227
+ error: `FETCH_ERROR: ${cause}`,
228
+ durationMs,
229
+ }
230
+ }
231
+ clearTimeout(timer)
232
+
233
+ let durationMs = Date.now() - t0
234
+
235
+ //HTTP非2xx, 原始回應本體放stderr供除錯與分類
236
+ if (!res.ok) {
237
+ return {
238
+ ok: false,
239
+ stdout: '',
240
+ stderr: strTruncate(txt, 1000, optTruncate),
241
+ code: res.status,
242
+ error: `HTTP ${res.status}`,
243
+ durationMs,
244
+ }
245
+ }
246
+
247
+ //取出choices[0]
248
+ let content = null
249
+ let finishReason = ''
250
+ let toolCalls = null
251
+ try {
252
+ let j = JSON.parse(txt)
253
+ content = get(j, 'choices.0.message.content', null)
254
+ finishReason = get(j, 'choices.0.finish_reason', '')
255
+ toolCalls = get(j, 'choices.0.message.tool_calls', null)
256
+ }
257
+ catch {}
258
+
259
+ //tool_calls, 本轉接器不支援工具迴圈(見檔頭), 明確回報而不假裝成功
260
+ //(Agnes於tool_calls時content為"\n\n"非null, 不攔截會靜默回傳空白內容)
261
+ if (finishReason === 'tool_calls' || (toolCalls !== null &amp;&amp; toolCalls !== undefined)) {
262
+ return {
263
+ ok: false,
264
+ stdout: '',
265
+ stderr: strTruncate(txt, 1000, optTruncate),
266
+ code: res.status,
267
+ error: 'TOOL_CALLS_UNSUPPORTED: use a cli kind (opencode/claude/codex/antigravity) when tools are needed',
268
+ durationMs,
269
+ }
270
+ }
271
+
272
+ if (content === null || content === undefined) {
273
+ return {
274
+ ok: false,
275
+ stdout: '',
276
+ stderr: strTruncate(txt, 500, optTruncate),
277
+ code: res.status,
278
+ error: 'INVALID_RESPONSE: missing choices[0].message.content',
279
+ durationMs,
280
+ }
281
+ }
282
+ if (typeof content !== 'string') {
283
+ content = JSON.stringify(content) //少數閘道回array形態
284
+ }
285
+
286
+ //validator, error與execCli一致令dispatchAiFallback可統一分流
287
+ if (validator &amp;&amp; !validator(content)) {
288
+ return {
289
+ ok: false,
290
+ stdout: strTruncate(content, 500, optTruncate),
291
+ stderr: '',
292
+ code: res.status,
293
+ error: 'OUTPUT_VALIDATION_FAILED',
294
+ durationMs,
295
+ }
296
+ }
297
+
298
+ return {
299
+ ok: true,
300
+ stdout: content,
301
+ stderr: '',
302
+ code: res.status,
303
+ error: '',
304
+ durationMs,
305
+ }
306
+ }
307
+
308
+
309
+ //本轉接器不使用execCli, 全部設定鍵自理, 未知鍵一律忽略
310
+
311
+
312
+ /**
313
+ * 以fetch直呼OpenAI相容API(chat/completions)呼叫AI模型
314
+ *
315
+ * 特點:
316
+ * 免安裝CLI、免預先登入,給baseURL+key+model即可呼叫(如OpenCode Zen、Agnes等OpenAI相容閘道);
317
+ * 僅供純文字生成(摘要、分析、改寫、產出JSON等素材已在prompt內之任務)——
318
+ * 需要讀本機檔案、grep、執行指令、抓網頁等工具能力時,請改用CLI類kind(opencode/claude/codex/antigravity);
319
+ * prompt走HTTP body,無命令列長度限制;
320
+ * 錯誤依HTTP狀態碼分流:4xx(429除外)為客戶端錯誤不重試,429/5xx/網路錯誤/逾時依maxRetries線性退避重試;
321
+ * 結果結構與逾時/驗證失敗之error字樣對齊execCli,可直接作為dispatchAi與dispatchAiFallback之kind('api-openai-compat')使用;
322
+ * 本函數不會reject,一律以結果物件之ok與error欄位回報成敗
323
+ *
324
+ * @param {String} prompt 輸入提示詞字串,作為user訊息置於HTTP body
325
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
326
+ * @param {String} opt.baseURL 輸入API基底網址字串,例如'https://opencode.ai/zen/v1'、'https://apihub.agnes-ai.com/v1',將於尾端接上/chat/completions
327
+ * @param {String} opt.model 輸入模型ID字串,例如'deepseek-v4-flash-free'(Zen之模型名不帶opencode/前綴)、'agnes-2.0-flash'
328
+ * @param {String} [opt.key=''] 輸入API key字串,以Bearer置於Authorization標頭,預設''代表不帶認證標頭
329
+ * @param {String} [opt.system=''] 輸入system提示詞字串,將以system角色置於messages首位,預設''代表不帶
330
+ * @param {Object} [opt.body={}] 輸入額外請求本體物件(如temperature、max_tokens、response_format),將併入預設body(同名鍵以此為準),預設{}。注意本轉接器不支援工具,帶入tools而模型回tool_calls時一律以TOOL_CALLS_UNSUPPORTED回報失敗,需要工具請改用CLI類kind
331
+ * @param {Object} [opt.headers={}] 輸入額外請求標頭物件,預設{}
332
+ * @param {Number} [opt.timeoutMs=120000] 輸入逾時毫秒正整數,逾時將中止請求(含回應串流讀取),預設120000
333
+ * @param {String|Function} [opt.validate=undefined] 輸入回覆內容驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,預設undefined代表不驗證
334
+ * @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,4xx(429除外)不重試,預設0
335
+ * @param {Number} [opt.retryDelayMs=5000] 輸入重試間隔毫秒正整數,實際間隔為retryDelayMs乘以重試次數且上限15000ms,預設5000
336
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(回覆內容字串)、stderr(失敗時之原始回應本體)、code(HTTP狀態碼,網路錯誤與逾時為null)、error(錯誤訊息字串,成功時為空字串)、durationMs(耗時毫秒)、attempts(實際嘗試次數),本函數不會reject
337
+ * @example
338
+ * //need network, no cli required
339
+ *
340
+ * import dispatchApiOpenaiCompat from './src/dispatchApiOpenaiCompat.mjs'
341
+ *
342
+ * let test = async () => {
343
+ *
344
+ * //OpenCode Zen(即opencode CLI之自家閘道), 模型名不帶opencode/前綴
345
+ * let r1 = await dispatchApiOpenaiCompat('請只回覆兩個字:完成', {
346
+ * baseURL: 'https://opencode.ai/zen/v1',
347
+ * key: 'sk-xxxxxx',
348
+ * model: 'deepseek-v4-flash-free',
349
+ * })
350
+ * console.log(r1.ok, r1.stdout.trim())
351
+ * // => true 完成
352
+ *
353
+ * //Agnes
354
+ * let r2 = await dispatchApiOpenaiCompat('請只回覆兩個字:完成', {
355
+ * baseURL: 'https://apihub.agnes-ai.com/v1',
356
+ * key: 'sk-xxxxxx',
357
+ * model: 'agnes-2.0-flash',
358
+ * })
359
+ * console.log(r2.ok, r2.stdout.trim())
360
+ * // => true 完成
361
+ *
362
+ * let re = await dispatchApiOpenaiCompat('abc', { baseURL: 'https://opencode.ai/zen/v1', key: 'sk-bad', model: 'deepseek-v4-flash-free' })
363
+ * console.log(re.ok, re.code, re.error)
364
+ * // => false 401 HTTP 401
365
+ *
366
+ * }
367
+ * await test()
368
+ * .catch((err) => {
369
+ * console.log(err)
370
+ * })
371
+ *
372
+ */
373
+ async function dispatchApiOpenaiCompat(prompt, opt = {}) {
374
+
375
+ //check prompt, 不reject故以錯誤結果物件回報
376
+ if (!isestr(prompt)) {
377
+ return getErrorResult('prompt must be a non-empty string')
378
+ }
379
+
380
+ //baseURL必填, API無CLI可回退
381
+ let baseURL = get(opt, 'baseURL', null)
382
+ if (!isestr(baseURL)) {
383
+ return getErrorResult('baseURL must be a non-empty string')
384
+ }
385
+
386
+ //model必填, chat/completions無預設模型
387
+ let model = get(opt, 'model', null)
388
+ if (!isestr(model)) {
389
+ return getErrorResult('model must be a non-empty string')
390
+ }
391
+
392
+ //key, 無效代表不帶認證標頭(部分閘道免認證)
393
+ let key = get(opt, 'key', null)
394
+
395
+ //system
396
+ let system = get(opt, 'system', null)
397
+
398
+ //bodyExtra
399
+ let bodyExtra = get(opt, 'body', null)
400
+ if (!isobj(bodyExtra)) {
401
+ bodyExtra = {}
402
+ }
403
+
404
+ //headersExtra
405
+ let headersExtra = get(opt, 'headers', null)
406
+ if (!isobj(headersExtra)) {
407
+ headersExtra = {}
408
+ }
409
+
410
+ //timeoutMs
411
+ let timeoutMs = get(opt, 'timeoutMs', null)
412
+ if (!ispint(timeoutMs)) {
413
+ timeoutMs = DEFAULT_TIMEOUT_MS
414
+ }
415
+ else {
416
+ timeoutMs = cint(timeoutMs)
417
+ }
418
+
419
+ //maxRetries
420
+ let maxRetries = get(opt, 'maxRetries', null)
421
+ if (!isp0int(maxRetries)) {
422
+ maxRetries = 0
423
+ }
424
+ else {
425
+ maxRetries = cint(maxRetries)
426
+ }
427
+
428
+ //retryDelayMs
429
+ let retryDelayMs = get(opt, 'retryDelayMs', null)
430
+ if (!ispint(retryDelayMs)) {
431
+ retryDelayMs = DEFAULT_RETRY_DELAY_MS
432
+ }
433
+ else {
434
+ retryDelayMs = cint(retryDelayMs)
435
+ }
436
+
437
+ //validator
438
+ let validator = buildValidator(get(opt, 'validate', null))
439
+
440
+ //url, baseURL尾端斜線正規化後接上端點
441
+ let url = baseURL.replace(/\/+$/, '') + '/chat/completions'
442
+
443
+ //messages
444
+ let messages = []
445
+ if (isestr(system)) {
446
+ messages.push({ role: 'system', content: system })
447
+ }
448
+ messages.push({ role: 'user', content: prompt })
449
+
450
+ //body, 額外鍵以bodyExtra為準(可覆寫temperature等, 覆寫messages屬進階用法)
451
+ let body = { model, messages, ...bodyExtra }
452
+
453
+ //headers
454
+ let headers = { 'Content-Type': 'application/json', ...headersExtra }
455
+ if (isestr(key)) {
456
+ headers['Authorization'] = `Bearer ${key}`
457
+ }
458
+
459
+ let lastResult = null
460
+ let totalAttempts = 0
461
+
462
+ for (let attempt = 0; attempt &lt;= maxRetries; attempt++) {
463
+
464
+ //delay, 重試間隔隨次數遞增, 上限15000ms(同execCli)
465
+ if (attempt > 0) {
466
+ await delay(Math.min(retryDelayMs * attempt, MAX_RETRY_DELAY_MS))
467
+ }
468
+
469
+ lastResult = await callOnce(url, headers, body, timeoutMs, validator)
470
+ totalAttempts = attempt + 1
471
+
472
+ if (lastResult.ok) {
473
+ lastResult.attempts = totalAttempts
474
+ return lastResult
475
+ }
476
+
477
+ //不可重試: 4xx(429除外)為客戶端錯誤, 重試無意義
478
+ let c = lastResult.code
479
+ if (isnum(c) &amp;&amp; c >= 400 &amp;&amp; c &lt; 500 &amp;&amp; c !== 429) {
480
+ break
481
+ }
482
+
483
+ }
484
+
485
+ lastResult.attempts = totalAttempts
486
+
487
+ return lastResult
488
+ }
489
+
490
+
491
+ export default dispatchApiOpenaiCompat
492
+ </code></pre>
493
+ </article>
494
+ </section>
495
+
496
+
497
+
498
+
499
+
500
+
501
+ </div>
502
+
503
+ <br class="clear">
504
+
505
+ <footer>
506
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Aug 13 2026 21:57:54 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
507
+ </footer>
508
+
509
+ <script>prettyPrint();</script>
510
+ <script src="scripts/polyfill.js"></script>
511
+ <script src="scripts/linenumber.js"></script>
512
+
513
+
514
+
515
+ </body>
516
+ </html>
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
31
 
32
- <h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#dispatchAi">dispatchAi</a></li><li><a href="global.html#dispatchAiFallback">dispatchAiFallback</a></li><li><a href="global.html#dispatchAntigravity">dispatchAntigravity</a></li><li><a href="global.html#dispatchClaude">dispatchClaude</a></li><li><a href="global.html#dispatchCodex">dispatchCodex</a></li><li><a href="global.html#dispatchOpencode">dispatchOpencode</a></li><li><a href="global.html#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li></ul>
32
+ <h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#buildChain">buildChain</a></li><li><a href="global.html#buildValidator">buildValidator</a></li><li><a href="global.html#callAiWithFallback">callAiWithFallback</a></li><li><a href="global.html#callOnce">callOnce</a></li><li><a href="global.html#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dispatchAi">dispatchAi</a></li><li><a href="global.html#dispatchAiFallback">dispatchAiFallback</a></li><li><a href="global.html#dispatchAiWkf">dispatchAiWkf</a></li><li><a href="global.html#dispatchAntigravity">dispatchAntigravity</a></li><li><a href="global.html#dispatchApiOpenaiCompat">dispatchApiOpenaiCompat</a></li><li><a href="global.html#dispatchClaude">dispatchClaude</a></li><li><a href="global.html#dispatchCodex">dispatchCodex</a></li><li><a href="global.html#dispatchOpencode">dispatchOpencode</a></li><li><a href="global.html#extractJsonLoose">extractJsonLoose</a></li><li><a href="global.html#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li><li><a href="global.html#runFanout">runFanout</a></li><li><a href="global.html#runFanoutPipeline">runFanoutPipeline</a></li><li><a href="global.html#runRolePipeline">runRolePipeline</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -177,7 +177,7 @@ export default dispatchClaude
177
177
  <br class="clear">
178
178
 
179
179
  <footer>
180
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Mon Aug 10 2026 09:36:49 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
180
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Aug 13 2026 21:57:54 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
181
181
  </footer>
182
182
 
183
183
  <script>prettyPrint();</script>
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
31
 
32
- <h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#dispatchAi">dispatchAi</a></li><li><a href="global.html#dispatchAiFallback">dispatchAiFallback</a></li><li><a href="global.html#dispatchAntigravity">dispatchAntigravity</a></li><li><a href="global.html#dispatchClaude">dispatchClaude</a></li><li><a href="global.html#dispatchCodex">dispatchCodex</a></li><li><a href="global.html#dispatchOpencode">dispatchOpencode</a></li><li><a href="global.html#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li></ul>
32
+ <h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#buildChain">buildChain</a></li><li><a href="global.html#buildValidator">buildValidator</a></li><li><a href="global.html#callAiWithFallback">callAiWithFallback</a></li><li><a href="global.html#callOnce">callOnce</a></li><li><a href="global.html#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dispatchAi">dispatchAi</a></li><li><a href="global.html#dispatchAiFallback">dispatchAiFallback</a></li><li><a href="global.html#dispatchAiWkf">dispatchAiWkf</a></li><li><a href="global.html#dispatchAntigravity">dispatchAntigravity</a></li><li><a href="global.html#dispatchApiOpenaiCompat">dispatchApiOpenaiCompat</a></li><li><a href="global.html#dispatchClaude">dispatchClaude</a></li><li><a href="global.html#dispatchCodex">dispatchCodex</a></li><li><a href="global.html#dispatchOpencode">dispatchOpencode</a></li><li><a href="global.html#extractJsonLoose">extractJsonLoose</a></li><li><a href="global.html#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li><li><a href="global.html#runFanout">runFanout</a></li><li><a href="global.html#runFanoutPipeline">runFanoutPipeline</a></li><li><a href="global.html#runRolePipeline">runRolePipeline</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -178,7 +178,7 @@ export default dispatchCodex
178
178
  <br class="clear">
179
179
 
180
180
  <footer>
181
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Mon Aug 10 2026 09:36:49 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
181
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Aug 13 2026 21:57:54 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
182
182
  </footer>
183
183
 
184
184
  <script>prettyPrint();</script>
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
31
 
32
- <h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#dispatchAi">dispatchAi</a></li><li><a href="global.html#dispatchAiFallback">dispatchAiFallback</a></li><li><a href="global.html#dispatchAntigravity">dispatchAntigravity</a></li><li><a href="global.html#dispatchClaude">dispatchClaude</a></li><li><a href="global.html#dispatchCodex">dispatchCodex</a></li><li><a href="global.html#dispatchOpencode">dispatchOpencode</a></li><li><a href="global.html#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li></ul>
32
+ <h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#buildChain">buildChain</a></li><li><a href="global.html#buildValidator">buildValidator</a></li><li><a href="global.html#callAiWithFallback">callAiWithFallback</a></li><li><a href="global.html#callOnce">callOnce</a></li><li><a href="global.html#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dispatchAi">dispatchAi</a></li><li><a href="global.html#dispatchAiFallback">dispatchAiFallback</a></li><li><a href="global.html#dispatchAiWkf">dispatchAiWkf</a></li><li><a href="global.html#dispatchAntigravity">dispatchAntigravity</a></li><li><a href="global.html#dispatchApiOpenaiCompat">dispatchApiOpenaiCompat</a></li><li><a href="global.html#dispatchClaude">dispatchClaude</a></li><li><a href="global.html#dispatchCodex">dispatchCodex</a></li><li><a href="global.html#dispatchOpencode">dispatchOpencode</a></li><li><a href="global.html#extractJsonLoose">extractJsonLoose</a></li><li><a href="global.html#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li><li><a href="global.html#runFanout">runFanout</a></li><li><a href="global.html#runFanoutPipeline">runFanoutPipeline</a></li><li><a href="global.html#runRolePipeline">runRolePipeline</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -247,7 +247,7 @@ export default dispatchOpencode
247
247
  <br class="clear">
248
248
 
249
249
  <footer>
250
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Mon Aug 10 2026 09:36:49 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
250
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Aug 13 2026 21:57:54 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
251
251
  </footer>
252
252
 
253
253
  <script>prettyPrint();</script>
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
31
 
32
- <h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#dispatchAi">dispatchAi</a></li><li><a href="global.html#dispatchAiFallback">dispatchAiFallback</a></li><li><a href="global.html#dispatchAntigravity">dispatchAntigravity</a></li><li><a href="global.html#dispatchClaude">dispatchClaude</a></li><li><a href="global.html#dispatchCodex">dispatchCodex</a></li><li><a href="global.html#dispatchOpencode">dispatchOpencode</a></li><li><a href="global.html#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li></ul>
32
+ <h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#buildChain">buildChain</a></li><li><a href="global.html#buildValidator">buildValidator</a></li><li><a href="global.html#callAiWithFallback">callAiWithFallback</a></li><li><a href="global.html#callOnce">callOnce</a></li><li><a href="global.html#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dispatchAi">dispatchAi</a></li><li><a href="global.html#dispatchAiFallback">dispatchAiFallback</a></li><li><a href="global.html#dispatchAiWkf">dispatchAiWkf</a></li><li><a href="global.html#dispatchAntigravity">dispatchAntigravity</a></li><li><a href="global.html#dispatchApiOpenaiCompat">dispatchApiOpenaiCompat</a></li><li><a href="global.html#dispatchClaude">dispatchClaude</a></li><li><a href="global.html#dispatchCodex">dispatchCodex</a></li><li><a href="global.html#dispatchOpencode">dispatchOpencode</a></li><li><a href="global.html#extractJsonLoose">extractJsonLoose</a></li><li><a href="global.html#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li><li><a href="global.html#runFanout">runFanout</a></li><li><a href="global.html#runFanoutPipeline">runFanoutPipeline</a></li><li><a href="global.html#runRolePipeline">runRolePipeline</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -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 Mon Aug 10 2026 09:36:49 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
116
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Aug 13 2026 21:57:54 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
117
117
  </footer>
118
118
 
119
119
  <script>prettyPrint();</script>
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
31
 
32
- <h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#dispatchAi">dispatchAi</a></li><li><a href="global.html#dispatchAiFallback">dispatchAiFallback</a></li><li><a href="global.html#dispatchAntigravity">dispatchAntigravity</a></li><li><a href="global.html#dispatchClaude">dispatchClaude</a></li><li><a href="global.html#dispatchCodex">dispatchCodex</a></li><li><a href="global.html#dispatchOpencode">dispatchOpencode</a></li><li><a href="global.html#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li></ul>
32
+ <h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#buildChain">buildChain</a></li><li><a href="global.html#buildValidator">buildValidator</a></li><li><a href="global.html#callAiWithFallback">callAiWithFallback</a></li><li><a href="global.html#callOnce">callOnce</a></li><li><a href="global.html#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dispatchAi">dispatchAi</a></li><li><a href="global.html#dispatchAiFallback">dispatchAiFallback</a></li><li><a href="global.html#dispatchAiWkf">dispatchAiWkf</a></li><li><a href="global.html#dispatchAntigravity">dispatchAntigravity</a></li><li><a href="global.html#dispatchApiOpenaiCompat">dispatchApiOpenaiCompat</a></li><li><a href="global.html#dispatchClaude">dispatchClaude</a></li><li><a href="global.html#dispatchCodex">dispatchCodex</a></li><li><a href="global.html#dispatchOpencode">dispatchOpencode</a></li><li><a href="global.html#extractJsonLoose">extractJsonLoose</a></li><li><a href="global.html#getCliArgs">getCliArgs</a></li><li><a href="global.html#getErrorResult">getErrorResult</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li><li><a href="global.html#runFanout">runFanout</a></li><li><a href="global.html#runFanoutPipeline">runFanoutPipeline</a></li><li><a href="global.html#runRolePipeline">runRolePipeline</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -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 Mon Aug 10 2026 09:36:49 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
105
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Aug 13 2026 21:57:54 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
106
106
  </footer>
107
107
 
108
108
  <script>prettyPrint();</script>