w-dispatch-ai 1.0.21 → 1.0.22

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 (49) hide show
  1. package/README.md +15 -3
  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 +5 -3
  5. package/docs/adapters.mjs.html +5 -3
  6. package/docs/budgetFor.mjs.html +2 -2
  7. package/docs/buildValidator.mjs.html +166 -0
  8. package/docs/castPintOr.mjs.html +2 -2
  9. package/docs/dfTimeoutMs.mjs.html +2 -2
  10. package/docs/dispatchAi.mjs.html +2 -2
  11. package/docs/dispatchAiFallback.mjs.html +2 -2
  12. package/docs/dispatchAiWkf.mjs.html +2 -2
  13. package/docs/dispatchAntigravity.mjs.html +2 -2
  14. package/docs/dispatchApiOpenaiCompat.mjs.html +4 -68
  15. package/docs/dispatchApiOpenaiResponses.mjs.html +498 -0
  16. package/docs/dispatchClaude.mjs.html +2 -2
  17. package/docs/dispatchCodex.mjs.html +2 -2
  18. package/docs/dispatchOpencode.mjs.html +2 -2
  19. package/docs/getCliArgs.mjs.html +2 -2
  20. package/docs/getErrorResult.mjs.html +2 -2
  21. package/docs/getErrorType.mjs.html +5 -3
  22. package/docs/global.html +2277 -1159
  23. package/docs/index.html +2 -2
  24. package/docs/readEnvFile.mjs.html +2 -2
  25. package/docs/resolveProviders.mjs.html +3 -3
  26. package/docs/wkf_callAiWithFallback.mjs.html +2 -2
  27. package/docs/wkf_createFileStore.mjs.html +2 -2
  28. package/docs/wkf_createUsageCounter.mjs.html +2 -2
  29. package/docs/wkf_extractJsonLoose.mjs.html +2 -2
  30. package/docs/wkf_noSideEffectPrefix.mjs.html +2 -2
  31. package/docs/wkf_runFanout.mjs.html +2 -2
  32. package/docs/wkf_runFanoutPipeline.mjs.html +2 -2
  33. package/docs/wkf_runRolePipeline.mjs.html +2 -2
  34. package/docs/wkf_salvageTruncatedArray.mjs.html +2 -2
  35. package/g.mjs +2 -2
  36. package/package.json +1 -1
  37. package/src/WDispatchAi.mjs +3 -1
  38. package/src/adapters.mjs +3 -1
  39. package/src/buildValidator.mjs +94 -0
  40. package/src/dispatchApiOpenaiCompat.mjs +2 -66
  41. package/src/dispatchApiOpenaiResponses.mjs +426 -0
  42. package/src/getErrorType.mjs +3 -1
  43. package/src/providers.mjs +303 -213
  44. package/src/resolveProviders.mjs +1 -1
  45. package/test/tools/fakeServerForApiTest.mjs +108 -3
  46. package/test/unit-WDispatchAi.test.mjs +10 -6
  47. package/test/unit-adapters.test.mjs +5 -3
  48. package/test/unit-dispatchAi.test.mjs +1 -1
  49. package/test/unit-dispatchApiOpenaiResponses.test.mjs +198 -0
@@ -1,14 +1,12 @@
1
1
  import get from 'lodash-es/get.js'
2
2
  import isobj from 'wsemi/src/isobj.mjs'
3
- import isfun from 'wsemi/src/isfun.mjs'
4
3
  import isnum from 'wsemi/src/isnum.mjs'
4
+ import cint from 'wsemi/src/cint.mjs'
5
5
  import isestr from 'wsemi/src/isestr.mjs'
6
6
  import isp0int from 'wsemi/src/isp0int.mjs'
7
- import cint from 'wsemi/src/cint.mjs'
8
7
  import delay from 'wsemi/src/delay.mjs'
9
8
  import castPintOr from './castPintOr.mjs'
10
- import strleft from 'wsemi/src/strleft.mjs'
11
- import strdelleft from 'wsemi/src/strdelleft.mjs'
9
+ import buildValidator from './buildValidator.mjs'
12
10
  import strTruncate from 'wsemi/src/strTruncate.mjs'
13
11
  import getErrorResult from './getErrorResult.mjs'
14
12
  import dfTimeoutMs from './dfTimeoutMs.mjs'
@@ -66,68 +64,6 @@ let optTruncate = {
66
64
  }
67
65
 
68
66
 
69
- /**
70
- * 建立驗證函式(規則語法同execCli之validate)
71
- *
72
- * @param {String|Function} rule 輸入驗證規則字串('nonempty'、'json'、'min:100', 逗號可串接)或自訂函式
73
- * @returns {Function|null} 回傳驗證函式,無有效規則回傳null
74
- */
75
- function buildValidator(rule) {
76
-
77
- //自訂函式直接使用
78
- if (isfun(rule)) {
79
- return rule
80
- }
81
-
82
- //check
83
- if (!isestr(rule)) {
84
- return null
85
- }
86
-
87
- //checks
88
- let checks = rule.split(',').map((r) => r.trim()).filter(Boolean)
89
- if (checks.length === 0) {
90
- return null
91
- }
92
-
93
- return (stdout) => {
94
- for (let check of checks) {
95
-
96
- if (check === 'nonempty') {
97
- if (!isestr(stdout) || stdout.trim() === '') {
98
- return false
99
- }
100
- }
101
-
102
- else if (check === 'json') {
103
- try {
104
- JSON.parse(stdout)
105
- }
106
- catch {
107
- return false
108
- }
109
- }
110
-
111
- else if (strleft(check, 4) === 'min:') {
112
-
113
- //規則本身無效(如min:abc) → 視為驗證失敗, 不靜默跳過
114
- let smin = strdelleft(check, 4)
115
- if (!isnum(smin)) {
116
- return false
117
- }
118
-
119
- let min = cint(smin)
120
- if (!isestr(stdout) || stdout.length < min) {
121
- return false
122
- }
123
- }
124
-
125
- }
126
- return true
127
- }
128
- }
129
-
130
-
131
67
  /**
132
68
  * 單次HTTP呼叫(內部使用, 不含重試邏輯)
133
69
  *
@@ -0,0 +1,426 @@
1
+ import get from 'lodash-es/get.js'
2
+ import isarr from 'wsemi/src/isarr.mjs'
3
+ import isobj from 'wsemi/src/isobj.mjs'
4
+ import isnum from 'wsemi/src/isnum.mjs'
5
+ import cint from 'wsemi/src/cint.mjs'
6
+ import isestr from 'wsemi/src/isestr.mjs'
7
+ import isp0int from 'wsemi/src/isp0int.mjs'
8
+ import delay from 'wsemi/src/delay.mjs'
9
+ import castPintOr from './castPintOr.mjs'
10
+ import buildValidator from './buildValidator.mjs'
11
+ import strTruncate from 'wsemi/src/strTruncate.mjs'
12
+ import getErrorResult from './getErrorResult.mjs'
13
+ import dfTimeoutMs from './dfTimeoutMs.mjs'
14
+
15
+
16
+ // dispatchApiOpenaiResponses.mjs — 以fetch直呼OpenAI Responses API(/responses)
17
+ //
18
+ // 【為何需要(2026-09-03實測後新增)】OpenCode Zen之端點「依模型家族而異」, 並非全部走
19
+ // chat/completions: 官方文件(https://opencode.ai/docs/zh-tw/zen/)之端點欄明載
20
+ // muse-spark系與GPT系走/responses(@ai-sdk/openai)、Claude系走/messages、Gemini系走
21
+ // /models/<id>, 僅deepseek/glm/kimi/minimax/nemotron/ling/mimo等走/chat/completions。
22
+ // 以dispatchApiOpenaiCompat打/responses型模型會得到HTTP 500而非404, 極易被誤判為
23
+ // 「模型故障」而反覆重試——實測muse-spark-1.2/1.3走/chat/completions連續10次500,
24
+ // 同金鑰同模型改打/responses立即200。本轉接器即為補上該端點型別。
25
+ //
26
+ // 【與chat/completions之協定差異(皆2026-09-03於Zen實測確認)】
27
+ // 請求: 單一輸入欄位input(字串或訊息陣列)而非messages; system提示走instructions;
28
+ // 輸出上限為max_output_tokens而非max_tokens。
29
+ // 回應: 無choices, 改為output陣列, 元素type可為'reasoning'(思考, 實測content為空)、
30
+ // 'message'(內容在content[].text, type為'output_text')、'function_call'(工具呼叫);
31
+ // 另有頂層status(completed/incomplete/failed)與incomplete_details。
32
+ // 用量: usage欄位名不同——input_tokens/output_tokens/total_tokens
33
+ // (chat/completions為prompt_tokens/completion_tokens/total_tokens)。
34
+ // 本套件usage一律原樣透傳不做正規化, 跨kind加總時呼叫端須自行對應欄位名。
35
+ //
36
+ // 【status不為completed一律視為失敗, 不回半截內容】incomplete(如max_output_tokens
37
+ // 耗盡)之output常為空陣列或截斷內容, 當成功回傳會讓截斷結果流入下游而無人察覺;
38
+ // 故以INCOMPLETE_RESPONSE回報(errorType為incomplete), 呼叫端據此調高max_output_tokens
39
+ // 或換家。實測: max_output_tokens為16時status為incomplete、incomplete_details為
40
+ // {reason:'max_output_tokens'}、output為空陣列。
41
+ //
42
+ // 【不支援工具, 與dispatchApiOpenaiCompat同一決策】output含function_call型元素時
43
+ // 以TOOL_CALLS_UNSUPPORTED回報而不假裝成功; 理由(工具迴圈須自建harness、tool_call
44
+ // 有會話束縛無法外傳上層agent)詳見dispatchApiOpenaiCompat.mjs與adapters.mjs檔頭。
45
+ //
46
+ // 【錯誤碼實測(Zen)】壞金鑰401(AuthError); 未知model亦回401(ModelError: Model X is not
47
+ // supported)而非404——故不可用狀態碼區分「金鑰錯」與「模型名錯」, 須讀stderr之訊息。
48
+ //
49
+ // 【結果結構對齊execCli】{ ok, stdout, stderr, code, error, errorType, durationMs,
50
+ // attempts, usage }, 與dispatchApiOpenaiCompat完全一致, 故dispatchAiFallback之
51
+ // 失敗分流與工作流層無須任何修改即可使用本kind。
52
+
53
+
54
+ //預設值
55
+ let DEFAULT_TIMEOUT_MS = dfTimeoutMs //全套件統一預設300000
56
+ let DEFAULT_RETRY_DELAY_MS = 5000
57
+ let MAX_RETRY_DELAY_MS = 15000
58
+
59
+
60
+ //optTruncate, 裁切失敗結果之內容時於刪節號後標註原始總長度(同execCli)
61
+ let optTruncate = {
62
+ funWithMsg: (str) => `(truncated, total ${str.length} chars)`,
63
+ }
64
+
65
+
66
+ /**
67
+ * 自Responses API之output陣列取出文字內容(僅取message型元素之output_text)
68
+ *
69
+ * reasoning型元素為模型思考過程(Zen實測其content為空陣列),不屬回覆內容故略過;
70
+ * 多個message元素依序串接
71
+ *
72
+ * @param {Array} output 輸入回應之output陣列
73
+ * @returns {String} 回傳串接後之文字內容,無有效內容回傳空字串
74
+ * @example
75
+ *
76
+ * import { extractOutputText } from './src/dispatchApiOpenaiResponses.mjs'
77
+ *
78
+ * let output = [
79
+ * { type: 'reasoning', content: [] },
80
+ * { type: 'message', content: [{ type: 'output_text', text: '完成' }] },
81
+ * ]
82
+ * console.log(extractOutputText(output))
83
+ * // => '完成'
84
+ *
85
+ */
86
+ function extractOutputText(output) {
87
+ if (!isarr(output)) {
88
+ return ''
89
+ }
90
+ let rs = []
91
+ for (let item of output) {
92
+ if (get(item, 'type', '') !== 'message') {
93
+ continue
94
+ }
95
+ let content = get(item, 'content', null)
96
+ if (!isarr(content)) {
97
+ continue
98
+ }
99
+ for (let c of content) {
100
+ let t = get(c, 'text', null)
101
+ if (isestr(t)) {
102
+ rs.push(t)
103
+ }
104
+ }
105
+ }
106
+ return rs.join('')
107
+ }
108
+
109
+
110
+ /**
111
+ * 單次HTTP呼叫(內部使用, 不含重試邏輯)
112
+ *
113
+ * @param {String} url 輸入完整端點網址字串
114
+ * @param {Object} headers 輸入請求標頭物件
115
+ * @param {Object} body 輸入請求本體物件
116
+ * @param {Number} timeoutMs 輸入逾時毫秒
117
+ * @param {Function|null} validator 輸入驗證函式
118
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件
119
+ */
120
+ async function callOnce(url, headers, body, timeoutMs, validator) {
121
+
122
+ let t0 = Date.now()
123
+
124
+ //mkResult, 結果形狀之單一來源(欄位對齊execCli, 追加errorType與usage),
125
+ //durationMs於呼叫當下計算; 失敗分支各自給errorType, 成功分支不帶(僅失敗結果有此欄)
126
+ let mkResult = (patch) => ({
127
+ ok: false,
128
+ stdout: '',
129
+ stderr: '',
130
+ code: null,
131
+ error: '',
132
+ durationMs: Date.now() - t0,
133
+ usage: null,
134
+ ...patch,
135
+ })
136
+
137
+ //AbortController, 逾時中止(含回應本體之串流讀取)
138
+ let controller = new AbortController()
139
+ let timer = setTimeout(() => {
140
+ controller.abort()
141
+ }, timeoutMs)
142
+
143
+ let res = null
144
+ let txt = ''
145
+ try {
146
+ res = await fetch(url, {
147
+ method: 'POST',
148
+ headers,
149
+ body: JSON.stringify(body),
150
+ signal: controller.signal,
151
+ })
152
+ txt = await res.text()
153
+ }
154
+ catch (err) {
155
+ clearTimeout(timer)
156
+
157
+ //逾時, error以TIMEOUT開頭令dispatchAiFallback視為與金鑰無關而跳組
158
+ if (err.name === 'AbortError') {
159
+ return mkResult({ error: `TIMEOUT after ${timeoutMs / 1000}s`, errorType: 'timeout' })
160
+ }
161
+
162
+ //網路層錯誤(DNS/連線拒絕等)
163
+ let cause = get(err, 'cause.code', '') || err.message
164
+ return mkResult({ error: `FETCH_ERROR: ${cause}`, errorType: 'fetch' })
165
+ }
166
+ clearTimeout(timer)
167
+
168
+ //HTTP非2xx, 原始回應本體放stderr供除錯與分類
169
+ //(Zen實測: 壞金鑰與未知model皆401, 故訊息須讀stderr而非僅看狀態碼)
170
+ if (!res.ok) {
171
+ return mkResult({
172
+ stderr: strTruncate(txt, 1000, optTruncate),
173
+ code: res.status,
174
+ error: `HTTP ${res.status}`,
175
+ errorType: 'http',
176
+ })
177
+ }
178
+
179
+ //解析output/status/usage(token用量原樣透傳; 失敗回應亦可能已耗token, 一併帶出)
180
+ let output = null
181
+ let status = ''
182
+ let incompleteReason = ''
183
+ let failMsg = ''
184
+ let usage = null
185
+ let parsed = true
186
+ try {
187
+ let j = JSON.parse(txt)
188
+ output = get(j, 'output', null)
189
+ status = get(j, 'status', '')
190
+ incompleteReason = get(j, 'incomplete_details.reason', '')
191
+ failMsg = get(j, 'error.message', '') || get(j, 'error.code', '')
192
+ usage = get(j, 'usage', null)
193
+ if (!isobj(usage)) {
194
+ usage = null
195
+ }
196
+ }
197
+ catch {
198
+ parsed = false
199
+ }
200
+ if (!parsed || !isarr(output)) {
201
+ return mkResult({
202
+ stderr: strTruncate(txt, 500, optTruncate),
203
+ code: res.status,
204
+ error: 'INVALID_RESPONSE: missing output array',
205
+ errorType: 'invalid-response',
206
+ usage,
207
+ })
208
+ }
209
+
210
+ //function_call, 本轉接器不支援工具迴圈(見檔頭), 明確回報而不假裝成功
211
+ let hasToolCall = output.some((o) => get(o, 'type', '') === 'function_call')
212
+ if (hasToolCall) {
213
+ return mkResult({
214
+ stderr: strTruncate(txt, 1000, optTruncate),
215
+ code: res.status,
216
+ error: 'TOOL_CALLS_UNSUPPORTED: use a cli kind (opencode/claude/codex/antigravity) when tools are needed',
217
+ errorType: 'tool-unsupported',
218
+ usage,
219
+ })
220
+ }
221
+
222
+ //status非completed一律失敗: incomplete之內容為截斷品, 當成功回傳會讓半截結果流入下游
223
+ if (status !== 'completed') {
224
+ let detail = incompleteReason || failMsg || status || 'unknown'
225
+ return mkResult({
226
+ stdout: strTruncate(extractOutputText(output), 500, optTruncate),
227
+ stderr: strTruncate(txt, 500, optTruncate),
228
+ code: res.status,
229
+ error: `INCOMPLETE_RESPONSE: status=${status || 'missing'} (${detail})`,
230
+ errorType: 'incomplete',
231
+ usage,
232
+ })
233
+ }
234
+
235
+ //content, 自output取message型之output_text
236
+ let content = extractOutputText(output)
237
+ if (content === '') {
238
+ return mkResult({
239
+ stderr: strTruncate(txt, 500, optTruncate),
240
+ code: res.status,
241
+ error: 'INVALID_RESPONSE: no output_text in output messages',
242
+ errorType: 'invalid-response',
243
+ usage,
244
+ })
245
+ }
246
+
247
+ //validator, error與execCli一致令dispatchAiFallback可統一分流
248
+ if (validator && !validator(content)) {
249
+ return mkResult({
250
+ stdout: strTruncate(content, 500, optTruncate),
251
+ code: res.status,
252
+ error: 'OUTPUT_VALIDATION_FAILED',
253
+ errorType: 'validation',
254
+ usage,
255
+ })
256
+ }
257
+
258
+ return mkResult({
259
+ ok: true,
260
+ stdout: content,
261
+ code: res.status,
262
+ usage,
263
+ })
264
+ }
265
+
266
+
267
+ //本轉接器不使用execCli, 全部設定鍵自理, 未知鍵一律忽略
268
+
269
+
270
+ /**
271
+ * 以fetch直呼OpenAI Responses API(/responses)呼叫AI模型
272
+ *
273
+ * 特點:
274
+ * 免安裝CLI、免預先登入,給baseURL+key+model即可呼叫(如OpenCode Zen之muse-spark系與GPT系);
275
+ * 端點型別與dispatchApiOpenaiCompat不同——Zen之端點依模型家族而異,收錄前須查官方文件端點欄
276
+ * (https://opencode.ai/docs/zh-tw/zen/),打錯端點會得到HTTP 500而非404,詳見providers.mjs檔頭;
277
+ * 僅供純文字生成,需要工具能力請改用CLI類kind(opencode/claude/codex/antigravity);
278
+ * status非completed(如max_output_tokens耗盡)一律以INCOMPLETE_RESPONSE回報,不回半截內容;
279
+ * 結果結構與dispatchApiOpenaiCompat完全一致,可直接作為dispatchAi與dispatchAiFallback之kind('api-openai-responses')使用;
280
+ * 本函數不會reject,一律以結果物件之ok與error欄位回報成敗
281
+ *
282
+ * @param {String} prompt 輸入提示詞字串,作為input置於HTTP body
283
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
284
+ * @param {String} opt.baseURL 輸入API基底網址字串,例如'https://opencode.ai/zen/v1',將於尾端接上/responses
285
+ * @param {String} opt.model 輸入模型ID字串,例如'muse-spark-1.3-contributor-free'
286
+ * @param {String} [opt.key=''] 輸入API key字串,以Bearer置於Authorization標頭,預設''代表不帶認證標頭
287
+ * @param {String} [opt.system=''] 輸入system提示詞字串,將置於instructions欄位(Responses API之system管道),預設''代表不帶
288
+ * @param {Object} [opt.body={}] 輸入額外請求本體物件(如temperature、max_output_tokens、reasoning),將併入預設body(同名鍵以此為準),預設{}。注意輸出上限欄位名為max_output_tokens而非max_tokens;本轉接器不支援工具,帶入tools而模型回function_call時一律以TOOL_CALLS_UNSUPPORTED回報失敗
289
+ * @param {Object} [opt.headers={}] 輸入額外請求標頭物件,預設{}
290
+ * @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將中止請求(含回應串流讀取),全套件統一預設300000
291
+ * @param {String|Function} [opt.validate=undefined] 輸入回覆內容驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,預設undefined代表不驗證
292
+ * @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,4xx(429除外)不重試,預設0
293
+ * @param {Number} [opt.retryDelayMs=5000] 輸入重試間隔毫秒正整數,實際間隔為retryDelayMs乘以重試次數且上限15000ms,預設5000
294
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(回覆內容字串)、stderr(失敗時之原始回應本體)、code(HTTP狀態碼,網路錯誤與逾時為null)、error(錯誤訊息字串,成功時為空字串)、errorType(僅失敗時,機器可讀錯誤類別字串,一覽見getErrorType.mjs檔頭)、durationMs(耗時毫秒)、attempts(實際嘗試次數)、usage(原始回應之token用量物件原樣透傳,欄位名為input_tokens/output_tokens/total_tokens,無則null),本函數不會reject
295
+ * @example
296
+ * //need network, no cli required
297
+ *
298
+ * import dispatchApiOpenaiResponses from './src/dispatchApiOpenaiResponses.mjs'
299
+ *
300
+ * let test = async () => {
301
+ *
302
+ * //OpenCode Zen之muse-spark系走/responses(非chat/completions), 詳見providers.mjs檔頭
303
+ * let r = await dispatchApiOpenaiResponses('請只回覆兩個字:完成', {
304
+ * baseURL: 'https://opencode.ai/zen/v1',
305
+ * key: 'sk-xxxxxx',
306
+ * model: 'muse-spark-1.3-contributor-free',
307
+ * })
308
+ * console.log(r.ok, r.stdout.trim())
309
+ * // => true 完成
310
+ *
311
+ * let re = await dispatchApiOpenaiResponses('abc', { baseURL: 'https://opencode.ai/zen/v1', key: 'sk-bad', model: 'muse-spark-1.3-contributor-free' })
312
+ * console.log(re.ok, re.code, re.errorType)
313
+ * // => false 401 http
314
+ *
315
+ * }
316
+ * await test()
317
+ * .catch((err) => {
318
+ * console.log(err)
319
+ * })
320
+ *
321
+ */
322
+ async function dispatchApiOpenaiResponses(prompt, opt = {}) {
323
+
324
+ //check prompt, 不reject故以錯誤結果物件回報
325
+ if (!isestr(prompt)) {
326
+ return getErrorResult('prompt must be a non-empty string')
327
+ }
328
+
329
+ //baseURL必填, API無CLI可回退
330
+ let baseURL = get(opt, 'baseURL', null)
331
+ if (!isestr(baseURL)) {
332
+ return getErrorResult('baseURL must be a non-empty string')
333
+ }
334
+
335
+ //model必填, responses無預設模型
336
+ let model = get(opt, 'model', null)
337
+ if (!isestr(model)) {
338
+ return getErrorResult('model must be a non-empty string')
339
+ }
340
+
341
+ //key, 無效代表不帶認證標頭(部分閘道免認證)
342
+ let key = get(opt, 'key', null)
343
+
344
+ //system, Responses API之system管道為instructions
345
+ let system = get(opt, 'system', null)
346
+
347
+ //bodyExtra
348
+ let bodyExtra = get(opt, 'body', null)
349
+ if (!isobj(bodyExtra)) {
350
+ bodyExtra = {}
351
+ }
352
+
353
+ //headersExtra
354
+ let headersExtra = get(opt, 'headers', null)
355
+ if (!isobj(headersExtra)) {
356
+ headersExtra = {}
357
+ }
358
+
359
+ //timeoutMs
360
+ let timeoutMs = castPintOr(get(opt, 'timeoutMs', null), DEFAULT_TIMEOUT_MS)
361
+
362
+ //maxRetries
363
+ let maxRetries = get(opt, 'maxRetries', null)
364
+ if (!isp0int(maxRetries)) {
365
+ maxRetries = 0
366
+ }
367
+ else {
368
+ maxRetries = cint(maxRetries)
369
+ }
370
+
371
+ //retryDelayMs
372
+ let retryDelayMs = castPintOr(get(opt, 'retryDelayMs', null), DEFAULT_RETRY_DELAY_MS)
373
+
374
+ //validator
375
+ let validator = buildValidator(get(opt, 'validate', null))
376
+
377
+ //url, baseURL尾端斜線正規化後接上端點
378
+ let url = baseURL.replace(/\/+$/, '') + '/responses'
379
+
380
+ //body, input為Responses API之輸入欄位; 額外鍵以bodyExtra為準(可覆寫temperature等)
381
+ let body = { model, input: prompt }
382
+ if (isestr(system)) {
383
+ body.instructions = system
384
+ }
385
+ body = { ...body, ...bodyExtra }
386
+
387
+ //headers
388
+ let headers = { 'Content-Type': 'application/json', ...headersExtra }
389
+ if (isestr(key)) {
390
+ headers['Authorization'] = `Bearer ${key}`
391
+ }
392
+
393
+ let lastResult = null
394
+ let totalAttempts = 0
395
+
396
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
397
+
398
+ //delay, 重試間隔隨次數遞增, 上限15000ms(同execCli)
399
+ if (attempt > 0) {
400
+ await delay(Math.min(retryDelayMs * attempt, MAX_RETRY_DELAY_MS))
401
+ }
402
+
403
+ lastResult = await callOnce(url, headers, body, timeoutMs, validator)
404
+ totalAttempts = attempt + 1
405
+
406
+ if (lastResult.ok) {
407
+ lastResult.attempts = totalAttempts
408
+ return lastResult
409
+ }
410
+
411
+ //不可重試: 4xx(429除外)為客戶端錯誤, 重試無意義
412
+ let c = lastResult.code
413
+ if (isnum(c) && c >= 400 && c < 500 && c !== 429) {
414
+ break
415
+ }
416
+
417
+ }
418
+
419
+ lastResult.attempts = totalAttempts
420
+
421
+ return lastResult
422
+ }
423
+
424
+
425
+ export default dispatchApiOpenaiResponses
426
+ export { extractOutputText }
@@ -22,7 +22,9 @@ import isestr from 'wsemi/src/isestr.mjs'
22
22
  // 'http' HTTP非2xx(code為狀態碼, 僅api類)
23
23
  // 'fetch' 網路層錯誤(DNS/連線拒絕, 僅api類)
24
24
  // 'tool-unsupported' 模型回tool_calls而本轉接器不支援工具(僅api類)
25
- // 'invalid-response' 回應缺choices[0].message.content(僅api類)
25
+ // 'invalid-response' 回應結構不合規(缺choices[0].message.content或缺output陣列, 僅api類)
26
+ // 'incomplete' Responses API之status非completed(如max_output_tokens耗盡而截斷,
27
+ // 僅api-openai-responses; 半截內容不當成功回傳, 見該轉接器檔頭)
26
28
  // 'aborted' shouldStop中止(僅dispatchAiFallback)
27
29
  // 'budget' 時間預算用盡(僅dispatchAiFallback)
28
30