w-dispatch-ai 1.0.36 → 1.0.38

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 (64) hide show
  1. package/README.md +26 -7
  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 +2 -2
  5. package/docs/adapters.mjs.html +2 -2
  6. package/docs/budgetFor.mjs.html +2 -2
  7. package/docs/buildValidator.mjs.html +2 -2
  8. package/docs/castPintOr.mjs.html +2 -2
  9. package/docs/checkTruncation.mjs.html +199 -0
  10. package/docs/describeNonJsonBody.mjs.html +122 -0
  11. package/docs/dfTimeoutMs.mjs.html +2 -2
  12. package/docs/dispatchAi.mjs.html +2 -2
  13. package/docs/dispatchAiFallback.mjs.html +42 -8
  14. package/docs/dispatchAiWkf.mjs.html +2 -2
  15. package/docs/dispatchAntigravity.mjs.html +2 -2
  16. package/docs/dispatchApiOpenaiCompat.mjs.html +124 -31
  17. package/docs/dispatchApiOpenaiResponses.mjs.html +113 -27
  18. package/docs/dispatchApiTypesafeSystemone.mjs.html +42 -17
  19. package/docs/dispatchClaude.mjs.html +2 -2
  20. package/docs/dispatchCodex.mjs.html +2 -2
  21. package/docs/dispatchOpencode.mjs.html +2 -2
  22. package/docs/getCliArgs.mjs.html +2 -2
  23. package/docs/getErrorResult.mjs.html +2 -2
  24. package/docs/getErrorType.mjs.html +9 -5
  25. package/docs/global.html +3159 -1806
  26. package/docs/index.html +2 -2
  27. package/docs/quota_dfQuotaTimeoutMs.mjs.html +2 -2
  28. package/docs/quota_fetchQuotaJson.mjs.html +2 -2
  29. package/docs/quota_fromCodexUsageHttp.mjs.html +2 -2
  30. package/docs/quota_getQuotaAntigravity.mjs.html +2 -2
  31. package/docs/quota_getQuotaClaude.mjs.html +2 -2
  32. package/docs/quota_getQuotaCodex.mjs.html +2 -2
  33. package/docs/quota_readJsonOrNull.mjs.html +2 -2
  34. package/docs/quota_toQuotaLabel.mjs.html +2 -2
  35. package/docs/quota_toQuotaResult.mjs.html +2 -2
  36. package/docs/quota_toQuotaScopedLabel.mjs.html +2 -2
  37. package/docs/quota_toQuotaWindow.mjs.html +2 -2
  38. package/docs/readEnvFile.mjs.html +2 -2
  39. package/docs/resolveProviders.mjs.html +2 -2
  40. package/docs/wkf_callAiWithFallback.mjs.html +64 -20
  41. package/docs/wkf_createFileStore.mjs.html +2 -2
  42. package/docs/wkf_createUsageCounter.mjs.html +2 -2
  43. package/docs/wkf_extractJsonLoose.mjs.html +2 -2
  44. package/docs/wkf_noSideEffectPrefix.mjs.html +2 -2
  45. package/docs/wkf_runFanout.mjs.html +4 -2
  46. package/docs/wkf_runFanoutPipeline.mjs.html +2 -2
  47. package/docs/wkf_runRolePipeline.mjs.html +2 -2
  48. package/docs/wkf_salvageTruncatedArray.mjs.html +13 -2
  49. package/package.json +1 -1
  50. package/src/checkTruncation.mjs +127 -0
  51. package/src/describeNonJsonBody.mjs +50 -0
  52. package/src/dispatchAiFallback.mjs +40 -6
  53. package/src/dispatchApiOpenaiCompat.mjs +122 -29
  54. package/src/dispatchApiOpenaiResponses.mjs +111 -25
  55. package/src/dispatchApiTypesafeSystemone.mjs +40 -15
  56. package/src/getErrorType.mjs +7 -3
  57. package/src/providers.mjs +1 -1
  58. package/src/wkf/callAiWithFallback.mjs +62 -18
  59. package/src/wkf/runFanout.mjs +2 -0
  60. package/src/wkf/salvageTruncatedArray.mjs +11 -0
  61. package/test/tools/fakeServerForApiTest.mjs +87 -2
  62. package/test/unit-dispatchApiOpenaiCompat.test.mjs +207 -1
  63. package/test/unit-dispatchApiOpenaiResponses.test.mjs +74 -5
  64. package/test/unit-dispatchApiTypesafeSystemone.test.mjs +25 -1
package/README.md CHANGED
@@ -211,6 +211,18 @@ await test()
211
211
  })
212
212
  ```
213
213
 
214
+ #### 行為變更紀錄(依賴方升版前請檢查):
215
+ **1.0.37 起**(皆為安裝方驗收時需逐一檢查之既有呼叫結果變化):
216
+ 1. **REST 文字類截斷預設失敗**:`finish_reason`為`length`/`content_filter`(Responses API 為`status: 'incomplete'`)時,於`validate`之前回`errorType: 'incomplete'`;此前會回`ok`或交`validate`判斷(不過時為`validation`)。**直接呼叫`dispatchAiFallback`或轉接器、且`validate`內含搶救策略者,須自行給`acceptTruncated: true`**;工作流`callAi`有自訂`parse`者自動同意、不必改。
217
+ 2. 截斷與`tool-unsupported`改為**整組跳過**(此前逐把換金鑰);截斷不重試。
218
+ 3. `validate`(及工作流之`parse`/`check`)拋錯改回驗證失敗(`validation`),不再令整條鏈 reject。
219
+ 4. 工作流層:條目自帶`validate`與工作流`validate`取交集(兩者皆過才算過)。
220
+ 5. 三個 REST 轉接器預設帶`Accept-Encoding: identity`。
221
+ 6. 依`errorType`做健康計數或監控者:截斷且驗證不過者由`validation`改為`incomplete`,須一併納入;結果另帶`finishReason`與`truncated`。
222
+
223
+ **1.0.37 之後**:
224
+ 7. HTTP 200 但本體非 JSON,另報`INVALID_RESPONSE: body is not JSON (<位元組數>, first bytes <前16位元組hex>, content-encoding=…, content-type=…)`(此前與「JSON 缺欄位」同一句),並改為**整組跳過**;`errorType`仍為`invalid-response`,「JSON 缺欄位」維持換金鑰。依`errorType`計數者若要把此類整組失敗計入,須納入`invalid-response`。
225
+
214
226
  #### Options shared by all dispatch functions:
215
227
  | key | type | default | description |
216
228
  | --- | --- | --- | --- |
@@ -298,9 +310,10 @@ Codex 0.149 起 Windows 預設走 elevated 沙箱(專用使用者 `CodexSandbo
298
310
  | `key` | String | `''` | API key,以`Bearer`置於`Authorization`標頭,省略代表不帶認證 |
299
311
  | `system` | String | `''` | system提示詞,置於messages首位 |
300
312
  | `body` | Object | `{}` | 額外請求本體(`temperature`、`max_tokens`、`response_format`等),同名鍵覆寫預設 |
301
- | `headers` | Object | `{}` | 額外請求標頭 |
313
+ | `headers` | Object | `{}` | 額外請求標頭,同名鍵覆寫預設。**預設帶 `Accept-Encoding: identity`**:伺服器若壓縮了回應卻漏標 `Content-Encoding`,Node 內建 fetch 不會解壓,本轉接器只拿到亂碼而回 `INVALID_RESPONSE`(三個 REST 轉接器同步)。重現條件:安裝方 2026-09-24 於 Zen 長回應(約 35 秒)實測,fetch 所見標頭為 0 個、本體 9,224 bytes 為 brotli,改帶 identity 則為 22,124 bytes 之 JSON;本機同日取樣 7 次未重現,推測漏標只在特定條件(長回應)出現。要改回允許壓縮可給 `{ 'Accept-Encoding': 'gzip, deflate, br' }` |
302
314
  | `timeoutMs` | Integer | `300000` | 逾時毫秒,逾時中止請求(含回應串流讀取);全套件統一預設 |
303
- | `maxRetries` | Integer | `0` | 失敗重試次數;**4xx(429除外)為客戶端錯誤不重試**,429/5xx/網路錯誤/逾時線性退避重試 |
315
+ | `maxRetries` | Integer | `0` | 失敗重試次數;**4xx(429除外)為客戶端錯誤不重試**,截斷(見`acceptTruncated`)亦不重試(同一請求必然再截斷),429/5xx/網路錯誤/逾時線性退避重試 |
316
+ | `acceptTruncated` | Boolean | `false` | **截斷預設失敗**:`finish_reason`為`length`或`content_filter`時,於`validate`之前回`errorType: 'incomplete'`(結果帶`truncated: true`,遞補層整組跳過)。`true`才放行`length`之截斷:有`validate`交其裁決、無則直接接受,結果仍標`truncated: true`;`content_filter`與可見輸出為空者一律失敗(空輸出之訊息附`reasoning_tokens`,常見於推理耗盡`max_tokens`)。`dispatchApiOpenaiResponses`同規則(`status: 'incomplete'`即截斷;`failed`不屬截斷)。**1.0.37 起行為改變**(此前截斷內容會交給`validate`判斷):**直接呼叫`dispatchAiFallback`或轉接器、且`validate`內含搶救策略者,須自行給`acceptTruncated: true`**(`dispatchAiFallback`會原樣轉傳給轉接器);工作流層`callAi`之預設見`salvageTruncatedArray`列 |
304
317
  | `retryDelayMs` | Integer | `5000` | 重試間隔,實際為`retryDelayMs`×次數且上限15000ms |
305
318
 
306
319
  結果結構對齊execCli:`stdout`為回覆內容、`code`為HTTP狀態碼(網路錯誤/逾時為`null`)、逾時`error`以`TIMEOUT`開頭、驗證失敗為`OUTPUT_VALIDATION_FAILED`——故可直接作為`dispatchAiFallback`條目(`kind: 'api-openai-compat'`,`keys`多金鑰輪替同樣適用)與工作流provider。
@@ -319,7 +332,8 @@ Codex 0.149 起 Windows 預設走 elevated 沙箱(專用使用者 `CodexSandbo
319
332
  | `http` | HTTP非2xx(`code`為狀態碼) | api類 |
320
333
  | `fetch` | 網路層錯誤(DNS/連線拒絕) | api類 |
321
334
  | `tool-unsupported` | 模型回tool_calls而api類不支援工具 | api類 |
322
- | `invalid-response` | 回應結構不合規(缺`choices[0].message.content`、缺`output`陣列,或 systemone 缺`answers`/缺所請求題目之答案) | api類 |
335
+ | `invalid-response` | 回應結構不合規(缺`choices[0].message.content`、缺`output`陣列,或 systemone 缺`answers`/缺所請求題目之答案);HTTP 200 但**本體非 JSON**時`error`另為`INVALID_RESPONSE: body is not JSON (<位元組數>, first bytes <前16位元組hex>, content-encoding=…, content-type=…)`,壓縮或損壞之本體可一眼辨識 | api類 |
336
+ | `incomplete` | 回應未完整:截斷(`finish_reason`為`length`/`content_filter`、Responses API 之`status: 'incomplete'`;結果帶`truncated: true`)或 Responses API 之其餘非完成狀態(如`failed`,`truncated: false`)。截斷判定只適用 REST 文字類;CLI 類拿不到終止訊號,截斷無從判別(已知限制) | api類 |
323
337
  | `aborted` | `shouldStop`中止 | fallback層 |
324
338
  | `budget` | 時間預算用盡 | fallback層 |
325
339
 
@@ -334,7 +348,7 @@ Codex 0.149 起 Windows 預設走 elevated 沙箱(專用使用者 `CodexSandbo
334
348
  | `baseURL` | String | `'https://api.typesafe.ai/v1'` | 將於尾端接上`/systemone` |
335
349
  | `model` | String | `'jev-latest'` | 另有`'jev-preview'`;回應之實際版本見結果之`modelResolved`(如`'jev-1.13.0'`) |
336
350
  | `key` | String | `''` | API key(`.env` 慣用 `TYPESAFE_KEYS`),以`Bearer`置於`Authorization`標頭 |
337
- | `body`/`headers` | Object | `{}` | 額外請求本體/標頭,同名鍵覆寫預設 |
351
+ | `body`/`headers` | Object | `{}` | 額外請求本體/標頭,同名鍵覆寫預設(標頭預設同 `dispatchApiOpenaiCompat` 帶 `Accept-Encoding: identity`) |
338
352
  | `timeoutMs`/`validate`/`maxRetries`/`retryDelayMs` | | | 同 `dispatchApiOpenaiCompat`(4xx 除 429 外不重試) |
339
353
 
340
354
  | 題型 `type` | `criteria` | 答案欄位 |
@@ -415,6 +429,9 @@ let r = await wdi.dispatchAiFallback(state, { providers: jev, questions })
415
429
  | 執行檔不存在 | `error`含`ENOENT` | 整組跳過 |
416
430
  | 參數錯誤 | `code === 2` | 整組跳過 |
417
431
  | 輸出未過驗證 | `error === 'OUTPUT_VALIDATION_FAILED'` | 整組跳過 |
432
+ | 截斷(REST 文字類) | `truncated === true` | 整組跳過(同模型同請求換金鑰必然再截斷;`status: 'failed'`不屬此列,照「其餘」換下一把) |
433
+ | 模型回工具呼叫而 api 類不支援 | `error`以`TOOL_CALLS_UNSUPPORTED`開頭 | 整組跳過 |
434
+ | HTTP 200 但本體非 JSON(api 類) | `error`以`INVALID_RESPONSE: body is not JSON`開頭 | 整組跳過(傳輸或閘道狀態;「JSON 缺欄位」不在此列,照「其餘」換下一把) |
418
435
  | kind無效 | `error`以`unknown ai kind`開頭 | 整組跳過 |
419
436
  | 其餘(含額度上限、金鑰無效、服務回錯) | — | 換組內下一把 |
420
437
 
@@ -467,13 +484,15 @@ let r = await wdi.dispatchAiFallback(state, { providers: jev, questions })
467
484
  keyIndex: 1, //實際使用之金鑰索引, 無keys時為null
468
485
  kind: 'api-openai-compat',
469
486
  model: 'agnes-2.0-flash',
470
- tried: [ //完整嘗試歷程, 成功時亦回傳; 失敗項另含stdout(被拒回覆)與stderr(錯誤輸出, 皆已截斷)供診斷
487
+ tried: [ //完整嘗試歷程, 成功時亦回傳; 失敗項另含stdout(被拒回覆)與stderr(錯誤輸出, 皆已截斷)供診斷; REST文字類各項另帶truncated與finishReason
471
488
  { providerId: 'agnes:agnes-2.0-flash', keyIndex: 0, keyId: 'agnes:agnes-2.0-flash#0', outcome: 'next-key', error: 'HTTP 401', durationMs: 105 },
472
489
  { providerId: 'agnes:agnes-2.0-flash', keyIndex: 1, keyId: 'agnes:agnes-2.0-flash#1', outcome: 'ok', durationMs: 1161 },
473
490
  ],
474
491
  }
475
492
  ```
476
493
 
494
+ **全數失敗時,頂層`error`/`errorType`只反映「最後一次」嘗試**:多把金鑰或多家依序失敗時(例如第一把本體非 JSON、第二把以剩餘預算重打而逾時),只記最終錯誤會誤判歸因。記日誌或評比時請一併記下各次嘗試,例如`` r.tried.filter((t) => t.outcome !== 'ok').map((t) => `${t.keyId}:${t.errorType}`) ``。
495
+
477
496
  #### dispatchAiWkf (workflow factory):
478
497
  注入一次provider定義表(名稱 → `dispatchAiFallback`條目)與共用預設,之後以名稱宣告工作流;名稱查無定義即回報錯誤(fail fast)。回覆經寬鬆JSON解析(`extractJsonLoose`)+自訂`check`驗證,非法回覆視為該家失敗而自動遞補;預設於prompt前掛「禁止建檔」約束(`promptPrefix: ''`可關閉);措辭豁免唯讀查閱——codex以shell讀檔,一律禁指令會令其無法讀取專案檔案且靜默回拒答(2026-08-13實測)。
479
498
 
@@ -606,7 +625,7 @@ let merged = [...wdi.providers.filter((p) => !extra.some((e) => e.id === p.id)),
606
625
  let resolved = wdi.resolveProviders(merged, { env, pick: [...] })
607
626
  ```
608
627
 
609
- **推理模型請放寬 `body.max_tokens`**:推理模型的 `max_tokens` 含推理 token(2026-09-24 實測 `space-bunny-free` 列 10 個縣市一題即用 5222,其中推理 4849),照抄上例之 8192 容易截斷,而 `api-openai-compat` 遇截斷仍回 `ok`(內容殘缺或為空);內建之 `zen:space-bunny-free` 即用 32768。
628
+ **推理模型請放寬 `body.max_tokens`**:推理模型的 `max_tokens` 含推理 token(2026-09-24 實測 `space-bunny-free` 列 10 個縣市一題即用 5222,其中推理 4849),照抄上例之 8192 容易截斷;截斷預設判失敗換家(`errorType: 'incomplete'`,見`acceptTruncated`),截斷頻繁等於白白換家;內建之 `zen:space-bunny-free` 即用 32768。
610
629
 
611
630
  **警語:動「輸入」、不要動「回傳」**——把條目 push 進回傳的 `providers` 陣列不會同步進 `table`,兩者當場分歧;合併輸入再呼叫則兩種輸出同源產出、必然一致。另同 id 重複條目屬設定錯誤(共用游標、日誌無法區分),合併時務必如上例先濾再接。
612
631
 
@@ -617,7 +636,7 @@ let resolved = wdi.resolveProviders(merged, { env, pick: [...] })
617
636
  | `createFileStore({ dir })` | `dispatchAiFallback`之`store`的檔案持久化——排程任務每次執行都是新行程,記憶體游標/冷卻每次歸零;本實作採**排除式passthrough**(state原封存還,僅剔自用欄位`at`),日後套件擴充state欄位自動相容(殷鑑:白名單store曾把1.0.7新增的`cooling`靜默丟棄) |
618
637
  | `createUsageCounter({ dir })` | 逐日逐鍵用量計帳,`onEvent`直接掛進dispatch即於`try`事件記帳;**純觀測絕不據以節流**(額度視窗形態多樣,臆測門檻擋自己的呼叫等同拿猜測當事實);排程環境務必注入`getDate`錨定時區 |
619
638
  | `budgetFor(chain)` | 遞補鏈走滿全鏈之時間預算(Σ各條目`timeoutMs`,未帶者以統一預設300000計);與外部排程硬上限取小者交`budgetMs` |
620
- | `salvageTruncatedArray(text)` | 截斷JSON陣列之前段搶救(救回的每個元素皆完整合法);**不併入預設解析**——「判失敗換家重產」與「搶救前段部分接受」是同一問題的兩種合法策略,組成自訂`parse`注入即可 |
639
+ | `salvageTruncatedArray(text)` | 截斷JSON陣列之前段搶救(救回的每個元素皆完整合法);**不併入預設解析**——「判失敗換家重產」與「搶救前段部分接受」是同一問題的兩種合法策略,組成自訂`parse`注入即可。REST 文字類截斷預設失敗,但工作流`callAi`之`acceptTruncated`預設為「有自訂`parse`且非`rawText`」,故注入自訂`parse`即同意接受截斷內容、既有用法不必改;結果之`truncated: true`可辨識救回的是半批;**直接呼叫`dispatchAiFallback`或轉接器、把搶救寫在`validate`裡者,須自行給`acceptTruncated: true`**(1.0.37 起,否則截斷在`validate`之前即判失敗)。限制:只救頂層元素為物件之陣列,從第一個`[`起算(外包物件如`{"items":[…`會救回內層陣列、前言含`[`會失效) |
621
640
  | `NO_SIDE_EFFECT` | 防副作用prompt前綴之單一來源(措辭含唯讀查閱豁免——codex以shell讀檔,一律禁指令等同禁讀檔);工作流`callAi`預設自動掛上,直呼`dispatchAiFallback`者自行前綴 |
622
641
 
623
642
  #### 訂閱額度查詢(quota):`getQuotaClaude`/`getQuotaCodex`/`getQuotaAntigravity`