w-dispatch-ai 1.0.35 → 1.0.37

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 (63) hide show
  1. package/README.md +13 -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/dfTimeoutMs.mjs.html +2 -2
  11. package/docs/dispatchAi.mjs.html +2 -2
  12. package/docs/dispatchAiFallback.mjs.html +33 -8
  13. package/docs/dispatchAiWkf.mjs.html +2 -2
  14. package/docs/dispatchAntigravity.mjs.html +2 -2
  15. package/docs/dispatchApiOpenaiCompat.mjs.html +98 -30
  16. package/docs/dispatchApiOpenaiResponses.mjs.html +102 -25
  17. package/docs/dispatchApiTypesafeSystemone.mjs.html +23 -15
  18. package/docs/dispatchClaude.mjs.html +2 -2
  19. package/docs/dispatchCodex.mjs.html +2 -2
  20. package/docs/dispatchOpencode.mjs.html +2 -2
  21. package/docs/getCliArgs.mjs.html +2 -2
  22. package/docs/getErrorResult.mjs.html +2 -2
  23. package/docs/getErrorType.mjs.html +6 -4
  24. package/docs/global.html +1237 -117
  25. package/docs/index.html +2 -2
  26. package/docs/quota_dfQuotaTimeoutMs.mjs.html +2 -2
  27. package/docs/quota_fetchQuotaJson.mjs.html +2 -2
  28. package/docs/quota_fromCodexUsageHttp.mjs.html +2 -2
  29. package/docs/quota_getQuotaAntigravity.mjs.html +2 -2
  30. package/docs/quota_getQuotaClaude.mjs.html +2 -2
  31. package/docs/quota_getQuotaCodex.mjs.html +2 -2
  32. package/docs/quota_readJsonOrNull.mjs.html +2 -2
  33. package/docs/quota_toQuotaLabel.mjs.html +2 -2
  34. package/docs/quota_toQuotaResult.mjs.html +2 -2
  35. package/docs/quota_toQuotaScopedLabel.mjs.html +2 -2
  36. package/docs/quota_toQuotaWindow.mjs.html +2 -2
  37. package/docs/readEnvFile.mjs.html +2 -2
  38. package/docs/resolveProviders.mjs.html +3 -3
  39. package/docs/wkf_callAiWithFallback.mjs.html +63 -20
  40. package/docs/wkf_createFileStore.mjs.html +2 -2
  41. package/docs/wkf_createUsageCounter.mjs.html +2 -2
  42. package/docs/wkf_extractJsonLoose.mjs.html +2 -2
  43. package/docs/wkf_noSideEffectPrefix.mjs.html +2 -2
  44. package/docs/wkf_runFanout.mjs.html +4 -2
  45. package/docs/wkf_runFanoutPipeline.mjs.html +2 -2
  46. package/docs/wkf_runRolePipeline.mjs.html +2 -2
  47. package/docs/wkf_salvageTruncatedArray.mjs.html +11 -2
  48. package/package.json +1 -1
  49. package/src/checkTruncation.mjs +127 -0
  50. package/src/dispatchAiFallback.mjs +31 -6
  51. package/src/dispatchApiOpenaiCompat.mjs +96 -28
  52. package/src/dispatchApiOpenaiResponses.mjs +100 -23
  53. package/src/dispatchApiTypesafeSystemone.mjs +21 -13
  54. package/src/getErrorType.mjs +4 -2
  55. package/src/providers.mjs +41 -8
  56. package/src/resolveProviders.mjs +1 -1
  57. package/src/wkf/callAiWithFallback.mjs +61 -18
  58. package/src/wkf/runFanout.mjs +2 -0
  59. package/src/wkf/salvageTruncatedArray.mjs +9 -0
  60. package/test/tools/fakeServerForApiTest.mjs +79 -2
  61. package/test/unit-dispatchApiOpenaiCompat.test.mjs +169 -1
  62. package/test/unit-dispatchApiOpenaiResponses.test.mjs +63 -5
  63. package/test/unit-dispatchApiTypesafeSystemone.test.mjs +17 -0
@@ -3,6 +3,7 @@ import dispatchApiOpenaiResponses, { extractOutputText } from '../src/dispatchAp
3
3
  import dispatchAi from '../src/dispatchAi.mjs'
4
4
  import dispatchAiFallback from '../src/dispatchAiFallback.mjs'
5
5
  import dispatchAiWkf from '../src/dispatchAiWkf.mjs'
6
+ import salvageTruncatedArray from '../src/wkf/salvageTruncatedArray.mjs'
6
7
  import fakeServerForApiTest from './tools/fakeServerForApiTest.mjs'
7
8
 
8
9
 
@@ -77,20 +78,77 @@ describe('dispatchApiOpenaiResponses', function() {
77
78
  assert.strict.deepEqual(r, rr)
78
79
  })
79
80
 
80
- it('status非completed一律失敗不回半截內容: incomplete與failed', async function() {
81
+ it('預設帶Accept-Encoding: identity——伺服器壓縮卻漏標Content-Encoding時仍可正確解析(同dispatchApiOpenaiCompat)', async function() {
82
+ let t = await dispatchApiOpenaiResponses('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'br-noheader' })
83
+ let r = [t.ok, t.code, t.stdout, t.error]
84
+ let rr = [true, 200, '完成', '']
85
+ assert.strict.deepEqual(r, rr)
86
+ })
87
+
88
+ it('status非completed預設失敗不回半截內容: incomplete(截斷, 無可見輸出另附說明)與failed(非截斷)', async function() {
81
89
  let t1 = await dispatchApiOpenaiResponses('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'incomplete' })
82
90
  let t2 = await dispatchApiOpenaiResponses('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'failed' })
83
91
  let r = [
84
- [t1.ok, t1.errorType, t1.error],
85
- [t2.ok, t2.errorType, t2.error],
92
+ [t1.ok, t1.errorType, t1.error, t1.truncated, t1.finishReason],
93
+ [t2.ok, t2.errorType, t2.error, t2.truncated],
94
+ ]
95
+ let rr = [
96
+ [false, 'incomplete', 'INCOMPLETE_RESPONSE: status=incomplete (max_output_tokens); no visible output (reasoning may have used up the output token limit)', true, 'length'],
97
+ [false, 'incomplete', 'INCOMPLETE_RESPONSE: status=failed (upstream blew up)', false],
98
+ ]
99
+ assert.strict.deepEqual(r, rr)
100
+ })
101
+
102
+ it('截斷(incomplete)帶部分文字預設失敗; acceptTruncated:true且validate通過則ok帶truncated; content_filter一律失敗; 重試遇截斷不重打', async function() {
103
+ let salvage = (s) => salvageTruncatedArray(s) !== null
104
+ let t1 = await dispatchApiOpenaiResponses('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'incomplete-partial' })
105
+ let t2 = await dispatchApiOpenaiResponses('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'incomplete-partial', acceptTruncated: true, validate: salvage })
106
+ let t3 = await dispatchApiOpenaiResponses('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'incomplete-filter', acceptTruncated: true })
107
+ let t4 = await dispatchApiOpenaiResponses('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'incomplete-partial', maxRetries: 2, retryDelayMs: 10 })
108
+ let r = [
109
+ [t1.ok, t1.errorType, t1.truncated, t1.finishReason, t1.error],
110
+ [t2.ok, t2.stdout, t2.truncated, t2.finishReason],
111
+ [t3.ok, t3.errorType, t3.truncated, t3.finishReason],
112
+ t4.attempts,
86
113
  ]
87
114
  let rr = [
88
- [false, 'incomplete', 'INCOMPLETE_RESPONSE: status=incomplete (max_output_tokens)'],
89
- [false, 'incomplete', 'INCOMPLETE_RESPONSE: status=failed (upstream blew up)'],
115
+ [false, 'incomplete', true, 'length', 'INCOMPLETE_RESPONSE: status=incomplete (max_output_tokens)'],
116
+ [true, '[{"a":1},{"b":2},{"c":', true, 'length'],
117
+ [false, 'incomplete', true, 'content_filter'],
118
+ 1,
90
119
  ]
91
120
  assert.strict.deepEqual(r, rr)
92
121
  })
93
122
 
123
+ it('failed與缺status非截斷(truncated為false), 遞補鏈照舊逐把換金鑰; 完成時finishReason為stop', async function() {
124
+ let t1 = await dispatchApiOpenaiResponses('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'no-status' })
125
+ let t2 = await dispatchApiOpenaiResponses('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'echo' })
126
+ let tf = await dispatchAiFallback('abc', {
127
+ providers: [{ id: 'g-failed', kind: 'api-openai-responses', baseURL: svr.url, model: 'failed', keys: ['sk-f0', 'sk-f1'] }],
128
+ })
129
+ let r = [
130
+ [t1.ok, t1.errorType, t1.truncated, t1.error],
131
+ [t2.ok, t2.truncated, t2.finishReason],
132
+ tf.tried.map((x) => [x.keyId, x.outcome]),
133
+ ]
134
+ let rr = [
135
+ [false, 'incomplete', false, 'INCOMPLETE_RESPONSE: status=missing (unknown)'],
136
+ [true, false, 'stop'],
137
+ [['g-failed#0', 'next-key'], ['g-failed#1', 'next-key']],
138
+ ]
139
+ assert.strict.deepEqual(r, rr)
140
+ })
141
+
142
+ it('validate拋錯視同驗證失敗, Promise照常resolve不reject', async function() {
143
+ let boom = () => {
144
+ throw new Error('boom-validate')
145
+ }
146
+ let t = await dispatchApiOpenaiResponses('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'echo', validate: boom })
147
+ let r = [t.ok, t.errorType, t.error, t.stderr.includes('boom-validate')]
148
+ let rr = [false, 'validation', 'OUTPUT_VALIDATION_FAILED', true]
149
+ assert.strict.deepEqual(r, rr)
150
+ })
151
+
94
152
  it('結構不合規之回應: 缺output陣列、非JSON、僅reasoning無message', async function() {
95
153
  let t1 = await dispatchApiOpenaiResponses('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'no-output' })
96
154
  let t2 = await dispatchApiOpenaiResponses('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'not-json' })
@@ -58,6 +58,23 @@ describe('dispatchApiTypesafeSystemone', function() {
58
58
  assert.strict.deepEqual(r, rr)
59
59
  })
60
60
 
61
+ it('預設帶Accept-Encoding: identity——伺服器壓縮卻漏標Content-Encoding時仍可正確解析(同dispatchApiOpenaiCompat)', async function() {
62
+ let t = await dispatchApiTypesafeSystemone('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'br-noheader', questions: Q })
63
+ let r = [t.ok, t.code, Object.keys(t.answers || {}), t.error]
64
+ let rr = [true, 200, ['category', 'urgent'], '']
65
+ assert.strict.deepEqual(r, rr)
66
+ })
67
+
68
+ it('validate拋錯視同驗證失敗, Promise照常resolve不reject(同dispatchApiOpenaiCompat)', async function() {
69
+ let boom = () => {
70
+ throw new Error('boom-validate')
71
+ }
72
+ let t = await dispatchApiTypesafeSystemone('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'echo', questions: Q, validate: boom })
73
+ let r = [t.ok, t.errorType, t.error, t.stderr.includes('boom-validate')]
74
+ let rr = [false, 'validation', 'OUTPUT_VALIDATION_FAILED', true]
75
+ assert.strict.deepEqual(r, rr)
76
+ })
77
+
61
78
  it('成功: stdout為answers之JSON字串且與answers一致, 追加modelResolved, usage原樣透傳', async function() {
62
79
  let t = await dispatchApiTypesafeSystemone('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'echo', questions: Q })
63
80
  let r = [t.ok, t.error, JSON.parse(t.stdout), Object.keys(t.answers), t.modelResolved, t.usage]