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.
- package/README.md +13 -7
- package/dist/w-dispatch-ai.umd.js +2 -2
- package/dist/w-dispatch-ai.umd.js.map +1 -1
- package/docs/WDispatchAi.mjs.html +2 -2
- package/docs/adapters.mjs.html +2 -2
- package/docs/budgetFor.mjs.html +2 -2
- package/docs/buildValidator.mjs.html +2 -2
- package/docs/castPintOr.mjs.html +2 -2
- package/docs/checkTruncation.mjs.html +199 -0
- package/docs/dfTimeoutMs.mjs.html +2 -2
- package/docs/dispatchAi.mjs.html +2 -2
- package/docs/dispatchAiFallback.mjs.html +33 -8
- package/docs/dispatchAiWkf.mjs.html +2 -2
- package/docs/dispatchAntigravity.mjs.html +2 -2
- package/docs/dispatchApiOpenaiCompat.mjs.html +98 -30
- package/docs/dispatchApiOpenaiResponses.mjs.html +102 -25
- package/docs/dispatchApiTypesafeSystemone.mjs.html +23 -15
- package/docs/dispatchClaude.mjs.html +2 -2
- package/docs/dispatchCodex.mjs.html +2 -2
- package/docs/dispatchOpencode.mjs.html +2 -2
- package/docs/getCliArgs.mjs.html +2 -2
- package/docs/getErrorResult.mjs.html +2 -2
- package/docs/getErrorType.mjs.html +6 -4
- package/docs/global.html +1237 -117
- package/docs/index.html +2 -2
- package/docs/quota_dfQuotaTimeoutMs.mjs.html +2 -2
- package/docs/quota_fetchQuotaJson.mjs.html +2 -2
- package/docs/quota_fromCodexUsageHttp.mjs.html +2 -2
- package/docs/quota_getQuotaAntigravity.mjs.html +2 -2
- package/docs/quota_getQuotaClaude.mjs.html +2 -2
- package/docs/quota_getQuotaCodex.mjs.html +2 -2
- package/docs/quota_readJsonOrNull.mjs.html +2 -2
- package/docs/quota_toQuotaLabel.mjs.html +2 -2
- package/docs/quota_toQuotaResult.mjs.html +2 -2
- package/docs/quota_toQuotaScopedLabel.mjs.html +2 -2
- package/docs/quota_toQuotaWindow.mjs.html +2 -2
- package/docs/readEnvFile.mjs.html +2 -2
- package/docs/resolveProviders.mjs.html +3 -3
- package/docs/wkf_callAiWithFallback.mjs.html +63 -20
- package/docs/wkf_createFileStore.mjs.html +2 -2
- package/docs/wkf_createUsageCounter.mjs.html +2 -2
- package/docs/wkf_extractJsonLoose.mjs.html +2 -2
- package/docs/wkf_noSideEffectPrefix.mjs.html +2 -2
- package/docs/wkf_runFanout.mjs.html +4 -2
- package/docs/wkf_runFanoutPipeline.mjs.html +2 -2
- package/docs/wkf_runRolePipeline.mjs.html +2 -2
- package/docs/wkf_salvageTruncatedArray.mjs.html +11 -2
- package/package.json +1 -1
- package/src/checkTruncation.mjs +127 -0
- package/src/dispatchAiFallback.mjs +31 -6
- package/src/dispatchApiOpenaiCompat.mjs +96 -28
- package/src/dispatchApiOpenaiResponses.mjs +100 -23
- package/src/dispatchApiTypesafeSystemone.mjs +21 -13
- package/src/getErrorType.mjs +4 -2
- package/src/providers.mjs +41 -8
- package/src/resolveProviders.mjs +1 -1
- package/src/wkf/callAiWithFallback.mjs +61 -18
- package/src/wkf/runFanout.mjs +2 -0
- package/src/wkf/salvageTruncatedArray.mjs +9 -0
- package/test/tools/fakeServerForApiTest.mjs +79 -2
- package/test/unit-dispatchApiOpenaiCompat.test.mjs +169 -1
- package/test/unit-dispatchApiOpenaiResponses.test.mjs +63 -5
- package/test/unit-dispatchApiTypesafeSystemone.test.mjs +17 -0
|
@@ -10,6 +10,7 @@ import buildValidator from './buildValidator.mjs'
|
|
|
10
10
|
import strTruncate from 'wsemi/src/strTruncate.mjs'
|
|
11
11
|
import getErrorResult from './getErrorResult.mjs'
|
|
12
12
|
import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
13
|
+
import { TRUNCATION_REASONS, normalizeFinishReason, safeValidate, judgeTruncated } from './checkTruncation.mjs'
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
// dispatchApiOpenaiCompat.mjs — 以fetch直呼OpenAI相容API(chat/completions)
|
|
@@ -39,17 +40,32 @@ import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
|
39
40
|
// 故呼叫端若於body帶入tools, 本函數一律以TOOL_CALLS_UNSUPPORTED回報失敗而不假裝成功
|
|
40
41
|
// (實測Agnes於tool_calls時content為"\n\n"而非null, 不特別處理會靜默回傳空白內容)。
|
|
41
42
|
//
|
|
42
|
-
//
|
|
43
|
+
// 【預設帶Accept-Encoding: identity(2026-09-24起, 三個REST轉接器同步)】Node內建fetch(undici)只在回應
|
|
44
|
+
// 帶Content-Encoding時才自動解壓; 伺服器若壓縮了本體卻漏標此標頭, fetch原樣交出壓縮位元組,
|
|
45
|
+
// JSON.parse失敗而回INVALID_RESPONSE(使用端回報Zen之space-bunny-free即此症: 手動brotli解壓得完整答案,
|
|
46
|
+
// 改帶identity即得正常JSON)。本機以假伺服器重現該機制; 惟同日對Zen取樣7次皆正確標示br(未重現漏標),
|
|
47
|
+
// 故屬防禦: identity請伺服器勿壓縮, 從源頭消除「壓縮處理不一致」一類失敗(不論成因在伺服器、代理或執行環境)。
|
|
48
|
+
// 代價僅傳輸量變大(實測Zen回應約5~8KB→20~65KB)。呼叫端可以opt.headers之'Accept-Encoding'覆寫。
|
|
49
|
+
//
|
|
50
|
+
// 【截斷(finish_reason為length或content_filter)預設失敗(2026-09-24起, 規則單一來源見checkTruncation.mjs)】
|
|
51
|
+
// 舊版不看finish_reason, 實測Zen之space-bunny-free於max_tokens:600時推理即耗盡而回content:""、
|
|
52
|
+
// 本轉接器卻回ok:true(靜默成功)。現於null檢查與validate之前裁定: 預設回INCOMPLETE_RESPONSE
|
|
53
|
+
// (errorType incomplete, 結果帶truncated:true), 不論validate為何——「validate接受」不代表內容完整。
|
|
54
|
+
// 呼叫端明示acceptTruncated:true才放行length之截斷(交validate裁決, 通過者仍標truncated:true);
|
|
55
|
+
// content_filter與可見輸出為空者一律失敗。finish_reason為stop/null/未知值者照舊(不可誤殺)。
|
|
56
|
+
//
|
|
57
|
+
// 【重試語意對齊execCli】4xx(429除外)為客戶端錯誤不可重試而立即中止; 截斷亦不重試(同一請求必然再截斷);
|
|
43
58
|
// 429/5xx/網路錯誤/逾時依maxRetries線性退避重試(間隔retryDelayMs*次數, 上限15000ms)。
|
|
44
59
|
//
|
|
45
60
|
// 【結果結構對齊execCli】{ ok, stdout, stderr, code, error, durationMs, attempts, usage },
|
|
46
61
|
// usage為原始回應之token用量原樣透傳(無則null; 驗證失敗等已耗token之失敗亦帶出),
|
|
47
62
|
// CLI類轉接器無可靠來源故無此欄——呼叫端可據此把「真實用量」與「只能估算」分開處理。
|
|
63
|
+
// 取得並解析回應後之結果另帶finishReason(正規化終止原因, 缺值為'')與truncated(是否截斷)。
|
|
48
64
|
// 失敗結果另帶機器可讀之errorType(timeout/fetch/http/tool-unsupported/invalid-response/
|
|
49
|
-
// validation/params, 一覽見getErrorType.mjs檔頭), error字串保留不動, 兩者並存。
|
|
65
|
+
// incomplete/validation/params, 一覽見getErrorType.mjs檔頭), error字串保留不動, 兩者並存。
|
|
50
66
|
// stdout為回覆內容、code為HTTP狀態碼(網路錯誤與逾時為null)、逾時error以TIMEOUT開頭、
|
|
51
|
-
// 驗證失敗error為OUTPUT_VALIDATION_FAILED
|
|
52
|
-
//
|
|
67
|
+
// 驗證失敗error為OUTPUT_VALIDATION_FAILED(validate拋錯亦同, 拋錯訊息置stderr)——
|
|
68
|
+
// dispatchAiFallback據此分流: 逾時/驗證失敗/截斷/工具不支援整組跳過, 其餘換金鑰。
|
|
53
69
|
|
|
54
70
|
|
|
55
71
|
//預設值
|
|
@@ -72,9 +88,10 @@ let optTruncate = {
|
|
|
72
88
|
* @param {Object} body 輸入請求本體物件
|
|
73
89
|
* @param {Number} timeoutMs 輸入逾時毫秒
|
|
74
90
|
* @param {Function|null} validator 輸入驗證函式
|
|
91
|
+
* @param {Boolean} acceptTruncated 輸入是否明示接受截斷內容
|
|
75
92
|
* @returns {Promise} 回傳Promise,resolve回傳結果物件
|
|
76
93
|
*/
|
|
77
|
-
async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
94
|
+
async function callOnce(url, headers, body, timeoutMs, validator, acceptTruncated) {
|
|
78
95
|
|
|
79
96
|
let t0 = Date.now()
|
|
80
97
|
|
|
@@ -134,13 +151,13 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
134
151
|
|
|
135
152
|
//取出choices[0]與usage(token用量, 原樣透傳; 失敗回應亦可能已耗token, 一併帶出)
|
|
136
153
|
let content = null
|
|
137
|
-
let
|
|
154
|
+
let finishReasonRaw = null
|
|
138
155
|
let toolCalls = null
|
|
139
156
|
let usage = null
|
|
140
157
|
try {
|
|
141
158
|
let j = JSON.parse(txt)
|
|
142
159
|
content = get(j, 'choices.0.message.content', null)
|
|
143
|
-
|
|
160
|
+
finishReasonRaw = get(j, 'choices.0.finish_reason', null)
|
|
144
161
|
toolCalls = get(j, 'choices.0.message.tool_calls', null)
|
|
145
162
|
usage = get(j, 'usage', null)
|
|
146
163
|
if (!isobj(usage)) {
|
|
@@ -149,6 +166,10 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
149
166
|
}
|
|
150
167
|
catch {}
|
|
151
168
|
|
|
169
|
+
//fin, 自此起之每個結果皆外顯正規化終止原因與是否截斷(規則見checkTruncation.mjs)
|
|
170
|
+
let finishReason = normalizeFinishReason(finishReasonRaw)
|
|
171
|
+
let fin = { finishReason, truncated: TRUNCATION_REASONS.includes(finishReason) }
|
|
172
|
+
|
|
152
173
|
//tool_calls, 本轉接器不支援工具迴圈(見檔頭), 明確回報而不假裝成功
|
|
153
174
|
//(Agnes於tool_calls時content為"\n\n"非null, 不攔截會靜默回傳空白內容)
|
|
154
175
|
if (finishReason === 'tool_calls' || (toolCalls !== null && toolCalls !== undefined)) {
|
|
@@ -158,31 +179,67 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
158
179
|
error: 'TOOL_CALLS_UNSUPPORTED: use a cli kind (opencode/claude/codex/antigravity) when tools are needed',
|
|
159
180
|
errorType: 'tool-unsupported',
|
|
160
181
|
usage,
|
|
182
|
+
...fin,
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
//content字串化(少數閘道回array形態); null/undefined一律視為無內容
|
|
187
|
+
if (content === undefined) {
|
|
188
|
+
content = null
|
|
189
|
+
}
|
|
190
|
+
if (content !== null && typeof content !== 'string') {
|
|
191
|
+
content = JSON.stringify(content)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
//截斷, 於null檢查與validate之前裁定: 預設失敗, 明示acceptTruncated才交validate放行(見檔頭)
|
|
195
|
+
if (fin.truncated) {
|
|
196
|
+
let d = judgeTruncated({
|
|
197
|
+
finishReason,
|
|
198
|
+
content,
|
|
199
|
+
acceptTruncated,
|
|
200
|
+
validator,
|
|
201
|
+
reasoningTokens: get(usage, 'completion_tokens_details.reasoning_tokens', null),
|
|
202
|
+
label: `finish_reason=${finishReason}`,
|
|
161
203
|
})
|
|
204
|
+
if (!d.accept) {
|
|
205
|
+
return mkResult({
|
|
206
|
+
stdout: strTruncate(content || '', 500, optTruncate),
|
|
207
|
+
stderr: strTruncate((d.threw ? `validate threw: ${d.threw}\n` : '') + txt, 500, optTruncate),
|
|
208
|
+
code: res.status,
|
|
209
|
+
error: d.error,
|
|
210
|
+
errorType: 'incomplete',
|
|
211
|
+
usage,
|
|
212
|
+
...fin,
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
return mkResult({ ok: true, stdout: content, code: res.status, usage, ...fin })
|
|
162
216
|
}
|
|
163
217
|
|
|
164
|
-
if (content === null
|
|
218
|
+
if (content === null) {
|
|
165
219
|
return mkResult({
|
|
166
220
|
stderr: strTruncate(txt, 500, optTruncate),
|
|
167
221
|
code: res.status,
|
|
168
222
|
error: 'INVALID_RESPONSE: missing choices[0].message.content',
|
|
169
223
|
errorType: 'invalid-response',
|
|
170
224
|
usage,
|
|
225
|
+
...fin,
|
|
171
226
|
})
|
|
172
227
|
}
|
|
173
|
-
if (typeof content !== 'string') {
|
|
174
|
-
content = JSON.stringify(content) //少數閘道回array形態
|
|
175
|
-
}
|
|
176
228
|
|
|
177
|
-
//validator, error與execCli一致令dispatchAiFallback
|
|
178
|
-
if (validator
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
229
|
+
//validator, error與execCli一致令dispatchAiFallback可統一分流; 拋錯視同拒絕(不reject), 訊息置stderr
|
|
230
|
+
if (validator) {
|
|
231
|
+
let v = safeValidate(validator, content)
|
|
232
|
+
if (!v.pass) {
|
|
233
|
+
return mkResult({
|
|
234
|
+
stdout: strTruncate(content, 500, optTruncate),
|
|
235
|
+
stderr: v.threw ? `validate threw: ${v.threw}` : '',
|
|
236
|
+
code: res.status,
|
|
237
|
+
error: 'OUTPUT_VALIDATION_FAILED',
|
|
238
|
+
errorType: 'validation',
|
|
239
|
+
usage,
|
|
240
|
+
...fin,
|
|
241
|
+
})
|
|
242
|
+
}
|
|
186
243
|
}
|
|
187
244
|
|
|
188
245
|
return mkResult({
|
|
@@ -190,6 +247,7 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
190
247
|
stdout: content,
|
|
191
248
|
code: res.status,
|
|
192
249
|
usage,
|
|
250
|
+
...fin,
|
|
193
251
|
})
|
|
194
252
|
}
|
|
195
253
|
|
|
@@ -216,12 +274,13 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
216
274
|
* @param {String} [opt.key=''] 輸入API key字串,以Bearer置於Authorization標頭,預設''代表不帶認證標頭
|
|
217
275
|
* @param {String} [opt.system=''] 輸入system提示詞字串,將以system角色置於messages首位,預設''代表不帶
|
|
218
276
|
* @param {Object} [opt.body={}] 輸入額外請求本體物件(如temperature、max_tokens、response_format),將併入預設body(同名鍵以此為準),預設{}。注意本轉接器不支援工具,帶入tools而模型回tool_calls時一律以TOOL_CALLS_UNSUPPORTED回報失敗,需要工具請改用CLI類kind
|
|
219
|
-
* @param {Object} [opt.headers={}]
|
|
277
|
+
* @param {Object} [opt.headers={}] 輸入額外請求標頭物件,同名鍵覆寫預設標頭;預設標頭含'Accept-Encoding: identity'(防伺服器壓縮卻漏標Content-Encoding,見檔頭),要改回允許壓縮可給{'Accept-Encoding':'gzip, deflate, br'},預設{}
|
|
220
278
|
* @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將中止請求(含回應串流讀取),全套件統一預設300000
|
|
221
|
-
* @param {String|Function} [opt.validate=undefined] 輸入回覆內容驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100'
|
|
222
|
-
* @param {
|
|
279
|
+
* @param {String|Function} [opt.validate=undefined] 輸入回覆內容驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,自訂函數拋錯視同驗證失敗,預設undefined代表不驗證
|
|
280
|
+
* @param {Boolean} [opt.acceptTruncated=false] 輸入是否接受被截斷(finish_reason為length)之內容布林值,true代表交validate裁決(無validate則直接接受)且結果標truncated:true;content_filter與可見輸出為空者一律失敗,預設false代表截斷一律失敗(errorType incomplete)
|
|
281
|
+
* @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,4xx(429除外)與截斷不重試,預設0
|
|
223
282
|
* @param {Number} [opt.retryDelayMs=5000] 輸入重試間隔毫秒正整數,實際間隔為retryDelayMs乘以重試次數且上限15000ms,預設5000
|
|
224
|
-
* @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(回覆內容字串)、stderr(失敗時之原始回應本體)、code(HTTP狀態碼,網路錯誤與逾時為null)、error(錯誤訊息字串,成功時為空字串)、errorType(僅失敗時,機器可讀錯誤類別字串,一覽見getErrorType.mjs檔頭)、durationMs(耗時毫秒)、attempts(實際嘗試次數)、usage(原始回應之token用量物件原樣透傳,無則null),本函數不會reject
|
|
283
|
+
* @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(回覆內容字串)、stderr(失敗時之原始回應本體)、code(HTTP狀態碼,網路錯誤與逾時為null)、error(錯誤訊息字串,成功時為空字串)、errorType(僅失敗時,機器可讀錯誤類別字串,一覽見getErrorType.mjs檔頭)、durationMs(耗時毫秒)、attempts(實際嘗試次數)、usage(原始回應之token用量物件原樣透傳,無則null)、finishReason(已取得回應時之正規化終止原因字串,缺值為'')、truncated(已取得回應時是否被截斷布林值),本函數不會reject
|
|
225
284
|
* @example
|
|
226
285
|
* //need network, no cli required
|
|
227
286
|
*
|
|
@@ -239,7 +298,8 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
239
298
|
* // => true 完成
|
|
240
299
|
*
|
|
241
300
|
* //OpenCode Zen(即opencode CLI之自家閘道), 模型名不帶opencode/前綴; 走此端點者見providers.mjs檔頭之端點表
|
|
242
|
-
* //注意Zen之免費模型自2026-09-17
|
|
301
|
+
* //注意Zen之免費模型自2026-09-17起多數禁止REST直呼(403 FreeTierError), 該類模型須改走opencode CLI(kind:'opencode');
|
|
302
|
+
* //閘門係逐模型套用(如space-bunny-free於2026-09-24實測REST仍200), 能否走REST以實測為準, 例外見providers.mjs檔頭
|
|
243
303
|
* let r2 = await dispatchApiOpenaiCompat('請只回覆兩個字:完成', {
|
|
244
304
|
* baseURL: 'https://opencode.ai/zen/v1',
|
|
245
305
|
* key: 'sk-xxxxxx',
|
|
@@ -314,6 +374,9 @@ async function dispatchApiOpenaiCompat(prompt, opt = {}) {
|
|
|
314
374
|
//validator
|
|
315
375
|
let validator = buildValidator(get(opt, 'validate', null))
|
|
316
376
|
|
|
377
|
+
//acceptTruncated, 僅明示true才放行截斷內容(見檔頭)
|
|
378
|
+
let acceptTruncated = get(opt, 'acceptTruncated', null) === true
|
|
379
|
+
|
|
317
380
|
//url, baseURL尾端斜線正規化後接上端點
|
|
318
381
|
let url = baseURL.replace(/\/+$/, '') + '/chat/completions'
|
|
319
382
|
|
|
@@ -327,8 +390,8 @@ async function dispatchApiOpenaiCompat(prompt, opt = {}) {
|
|
|
327
390
|
//body, 額外鍵以bodyExtra為準(可覆寫temperature等, 覆寫messages屬進階用法)
|
|
328
391
|
let body = { model, messages, ...bodyExtra }
|
|
329
392
|
|
|
330
|
-
//headers
|
|
331
|
-
let headers = { 'Content-Type': 'application/json', ...headersExtra }
|
|
393
|
+
//headers, Accept-Encoding預設identity(防伺服器壓縮卻漏標Content-Encoding, 見檔頭), headersExtra同名鍵可覆寫
|
|
394
|
+
let headers = { 'Content-Type': 'application/json', 'Accept-Encoding': 'identity', ...headersExtra }
|
|
332
395
|
if (isestr(key)) {
|
|
333
396
|
headers['Authorization'] = `Bearer ${key}`
|
|
334
397
|
}
|
|
@@ -343,7 +406,7 @@ async function dispatchApiOpenaiCompat(prompt, opt = {}) {
|
|
|
343
406
|
await delay(Math.min(retryDelayMs * attempt, MAX_RETRY_DELAY_MS))
|
|
344
407
|
}
|
|
345
408
|
|
|
346
|
-
lastResult = await callOnce(url, headers, body, timeoutMs, validator)
|
|
409
|
+
lastResult = await callOnce(url, headers, body, timeoutMs, validator, acceptTruncated)
|
|
347
410
|
totalAttempts = attempt + 1
|
|
348
411
|
|
|
349
412
|
if (lastResult.ok) {
|
|
@@ -351,6 +414,11 @@ async function dispatchApiOpenaiCompat(prompt, opt = {}) {
|
|
|
351
414
|
return lastResult
|
|
352
415
|
}
|
|
353
416
|
|
|
417
|
+
//不可重試: 截斷為決定性(同一請求必然再截斷, 重試只會再燒一次輸出上限)
|
|
418
|
+
if (lastResult.truncated === true) {
|
|
419
|
+
break
|
|
420
|
+
}
|
|
421
|
+
|
|
354
422
|
//不可重試: 4xx(429除外)為客戶端錯誤, 重試無意義
|
|
355
423
|
let c = lastResult.code
|
|
356
424
|
if (isnum(c) && c >= 400 && c < 500 && c !== 429) {
|
|
@@ -11,6 +11,7 @@ import buildValidator from './buildValidator.mjs'
|
|
|
11
11
|
import strTruncate from 'wsemi/src/strTruncate.mjs'
|
|
12
12
|
import getErrorResult from './getErrorResult.mjs'
|
|
13
13
|
import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
14
|
+
import { normalizeFinishReason, safeValidate, judgeTruncated } from './checkTruncation.mjs'
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
// dispatchApiOpenaiResponses.mjs — 以fetch直呼OpenAI Responses API(/responses)
|
|
@@ -33,16 +34,23 @@ import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
|
33
34
|
// (chat/completions為prompt_tokens/completion_tokens/total_tokens)。
|
|
34
35
|
// 本套件usage一律原樣透傳不做正規化, 跨kind加總時呼叫端須自行對應欄位名。
|
|
35
36
|
//
|
|
36
|
-
// 【status不為completed
|
|
37
|
+
// 【status不為completed預設失敗, 不回半截內容】incomplete(如max_output_tokens
|
|
37
38
|
// 耗盡)之output常為空陣列或截斷內容, 當成功回傳會讓截斷結果流入下游而無人察覺;
|
|
38
39
|
// 故以INCOMPLETE_RESPONSE回報(errorType為incomplete), 呼叫端據此調高max_output_tokens
|
|
39
40
|
// 或換家。實測: max_output_tokens為16時status為incomplete、incomplete_details為
|
|
40
41
|
// {reason:'max_output_tokens'}、output為空陣列。
|
|
42
|
+
// 2026-09-24起與dispatchApiOpenaiCompat同一截斷規則(單一來源checkTruncation.mjs): 僅status為incomplete
|
|
43
|
+
// 視為截斷(結果帶truncated:true, finishReason將max_output_tokens正規化為length), 呼叫端明示
|
|
44
|
+
// acceptTruncated:true才放行length之截斷(交validate裁決); failed/缺status/處理中等屬非截斷之失敗,
|
|
45
|
+
// truncated:false, 遞補層照舊換金鑰(服務回錯)。可見輸出為空之截斷訊息附reasoning_tokens。
|
|
41
46
|
//
|
|
42
47
|
// 【不支援工具, 與dispatchApiOpenaiCompat同一決策】output含function_call型元素時
|
|
43
48
|
// 以TOOL_CALLS_UNSUPPORTED回報而不假裝成功; 理由(工具迴圈須自建harness、tool_call
|
|
44
49
|
// 有會話束縛無法外傳上層agent)詳見dispatchApiOpenaiCompat.mjs與adapters.mjs檔頭。
|
|
45
50
|
//
|
|
51
|
+
// 【預設帶Accept-Encoding: identity(2026-09-24起)】防伺服器壓縮卻漏標Content-Encoding而令fetch不解壓、
|
|
52
|
+
// JSON.parse失敗; 與dispatchApiOpenaiCompat同一決策與依據(見該檔檔頭), opt.headers可覆寫。
|
|
53
|
+
//
|
|
46
54
|
// 【錯誤碼實測(Zen)】壞金鑰401(AuthError); 未知model亦回401(ModelError: Model X is not
|
|
47
55
|
// supported)而非404——故不可用狀態碼區分「金鑰錯」與「模型名錯」, 須讀stderr之訊息。
|
|
48
56
|
//
|
|
@@ -115,9 +123,10 @@ function extractOutputText(output) {
|
|
|
115
123
|
* @param {Object} body 輸入請求本體物件
|
|
116
124
|
* @param {Number} timeoutMs 輸入逾時毫秒
|
|
117
125
|
* @param {Function|null} validator 輸入驗證函式
|
|
126
|
+
* @param {Boolean} acceptTruncated 輸入是否明示接受截斷內容
|
|
118
127
|
* @returns {Promise} 回傳Promise,resolve回傳結果物件
|
|
119
128
|
*/
|
|
120
|
-
async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
129
|
+
async function callOnce(url, headers, body, timeoutMs, validator, acceptTruncated) {
|
|
121
130
|
|
|
122
131
|
let t0 = Date.now()
|
|
123
132
|
|
|
@@ -197,7 +206,7 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
197
206
|
catch {
|
|
198
207
|
parsed = false
|
|
199
208
|
}
|
|
200
|
-
if (!parsed
|
|
209
|
+
if (!parsed) {
|
|
201
210
|
return mkResult({
|
|
202
211
|
stderr: strTruncate(txt, 500, optTruncate),
|
|
203
212
|
code: res.status,
|
|
@@ -207,8 +216,23 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
207
216
|
})
|
|
208
217
|
}
|
|
209
218
|
|
|
219
|
+
//fin, 自此起之每個結果皆外顯正規化終止原因與是否截斷(與chat/completions對齊, 規則見checkTruncation.mjs):
|
|
220
|
+
//completed→stop; incomplete→依reason(max_output_tokens→length、content_filter原值、缺→incomplete); 其餘status原值; 缺status→''
|
|
221
|
+
let statusN = normalizeFinishReason(status)
|
|
222
|
+
let reasonN = normalizeFinishReason(incompleteReason)
|
|
223
|
+
let finishReason = statusN
|
|
224
|
+
if (statusN === 'completed') {
|
|
225
|
+
finishReason = 'stop'
|
|
226
|
+
}
|
|
227
|
+
else if (statusN === 'incomplete') {
|
|
228
|
+
finishReason = (reasonN === 'max_output_tokens') ? 'length' : (reasonN || 'incomplete')
|
|
229
|
+
}
|
|
230
|
+
let fin = { finishReason, truncated: statusN === 'incomplete' }
|
|
231
|
+
let detail = incompleteReason || failMsg || status || 'unknown'
|
|
232
|
+
let isArr = isarr(output)
|
|
233
|
+
|
|
210
234
|
//function_call, 本轉接器不支援工具迴圈(見檔頭), 明確回報而不假裝成功
|
|
211
|
-
let hasToolCall = output.some((o) => get(o, 'type', '') === 'function_call')
|
|
235
|
+
let hasToolCall = isArr && output.some((o) => get(o, 'type', '') === 'function_call')
|
|
212
236
|
if (hasToolCall) {
|
|
213
237
|
return mkResult({
|
|
214
238
|
stderr: strTruncate(txt, 1000, optTruncate),
|
|
@@ -216,12 +240,48 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
216
240
|
error: 'TOOL_CALLS_UNSUPPORTED: use a cli kind (opencode/claude/codex/antigravity) when tools are needed',
|
|
217
241
|
errorType: 'tool-unsupported',
|
|
218
242
|
usage,
|
|
243
|
+
...fin,
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
//截斷(status為incomplete), 於output檢查與validate之前裁定: 預設失敗, 明示acceptTruncated才交validate放行(見檔頭)
|
|
248
|
+
if (fin.truncated) {
|
|
249
|
+
let partial = isArr ? extractOutputText(output) : ''
|
|
250
|
+
let d = judgeTruncated({
|
|
251
|
+
finishReason,
|
|
252
|
+
content: partial,
|
|
253
|
+
acceptTruncated,
|
|
254
|
+
validator,
|
|
255
|
+
reasoningTokens: get(usage, 'output_tokens_details.reasoning_tokens', null),
|
|
256
|
+
label: `status=${status} (${detail})`,
|
|
257
|
+
})
|
|
258
|
+
if (!d.accept) {
|
|
259
|
+
return mkResult({
|
|
260
|
+
stdout: strTruncate(partial, 500, optTruncate),
|
|
261
|
+
stderr: strTruncate((d.threw ? `validate threw: ${d.threw}\n` : '') + txt, 500, optTruncate),
|
|
262
|
+
code: res.status,
|
|
263
|
+
error: d.error,
|
|
264
|
+
errorType: 'incomplete',
|
|
265
|
+
usage,
|
|
266
|
+
...fin,
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
return mkResult({ ok: true, stdout: partial, code: res.status, usage, ...fin })
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!isArr) {
|
|
273
|
+
return mkResult({
|
|
274
|
+
stderr: strTruncate(txt, 500, optTruncate),
|
|
275
|
+
code: res.status,
|
|
276
|
+
error: 'INVALID_RESPONSE: missing output array',
|
|
277
|
+
errorType: 'invalid-response',
|
|
278
|
+
usage,
|
|
279
|
+
...fin,
|
|
219
280
|
})
|
|
220
281
|
}
|
|
221
282
|
|
|
222
|
-
|
|
223
|
-
if (
|
|
224
|
-
let detail = incompleteReason || failMsg || status || 'unknown'
|
|
283
|
+
//其餘非completed(failed/缺status/處理中等)屬非截斷之失敗, 不回半截內容; truncated:false故遞補層照舊換金鑰
|
|
284
|
+
if (statusN !== 'completed') {
|
|
225
285
|
return mkResult({
|
|
226
286
|
stdout: strTruncate(extractOutputText(output), 500, optTruncate),
|
|
227
287
|
stderr: strTruncate(txt, 500, optTruncate),
|
|
@@ -229,6 +289,7 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
229
289
|
error: `INCOMPLETE_RESPONSE: status=${status || 'missing'} (${detail})`,
|
|
230
290
|
errorType: 'incomplete',
|
|
231
291
|
usage,
|
|
292
|
+
...fin,
|
|
232
293
|
})
|
|
233
294
|
}
|
|
234
295
|
|
|
@@ -241,18 +302,24 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
241
302
|
error: 'INVALID_RESPONSE: no output_text in output messages',
|
|
242
303
|
errorType: 'invalid-response',
|
|
243
304
|
usage,
|
|
305
|
+
...fin,
|
|
244
306
|
})
|
|
245
307
|
}
|
|
246
308
|
|
|
247
|
-
//validator, error與execCli一致令dispatchAiFallback
|
|
248
|
-
if (validator
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
309
|
+
//validator, error與execCli一致令dispatchAiFallback可統一分流; 拋錯視同拒絕(不reject), 訊息置stderr
|
|
310
|
+
if (validator) {
|
|
311
|
+
let v = safeValidate(validator, content)
|
|
312
|
+
if (!v.pass) {
|
|
313
|
+
return mkResult({
|
|
314
|
+
stdout: strTruncate(content, 500, optTruncate),
|
|
315
|
+
stderr: v.threw ? `validate threw: ${v.threw}` : '',
|
|
316
|
+
code: res.status,
|
|
317
|
+
error: 'OUTPUT_VALIDATION_FAILED',
|
|
318
|
+
errorType: 'validation',
|
|
319
|
+
usage,
|
|
320
|
+
...fin,
|
|
321
|
+
})
|
|
322
|
+
}
|
|
256
323
|
}
|
|
257
324
|
|
|
258
325
|
return mkResult({
|
|
@@ -260,6 +327,7 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
260
327
|
stdout: content,
|
|
261
328
|
code: res.status,
|
|
262
329
|
usage,
|
|
330
|
+
...fin,
|
|
263
331
|
})
|
|
264
332
|
}
|
|
265
333
|
|
|
@@ -286,12 +354,13 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
286
354
|
* @param {String} [opt.key=''] 輸入API key字串,以Bearer置於Authorization標頭,預設''代表不帶認證標頭
|
|
287
355
|
* @param {String} [opt.system=''] 輸入system提示詞字串,將置於instructions欄位(Responses API之system管道),預設''代表不帶
|
|
288
356
|
* @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={}]
|
|
357
|
+
* @param {Object} [opt.headers={}] 輸入額外請求標頭物件,同名鍵覆寫預設標頭;預設標頭含'Accept-Encoding: identity'(防伺服器壓縮卻漏標Content-Encoding,見檔頭),預設{}
|
|
290
358
|
* @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將中止請求(含回應串流讀取),全套件統一預設300000
|
|
291
|
-
* @param {String|Function} [opt.validate=undefined] 輸入回覆內容驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100'
|
|
292
|
-
* @param {
|
|
359
|
+
* @param {String|Function} [opt.validate=undefined] 輸入回覆內容驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,自訂函數拋錯視同驗證失敗,預設undefined代表不驗證
|
|
360
|
+
* @param {Boolean} [opt.acceptTruncated=false] 輸入是否接受被截斷(status為incomplete且reason為max_output_tokens)之內容布林值,true代表交validate裁決(無validate則直接接受)且結果標truncated:true;content_filter與可見輸出為空者一律失敗,預設false代表截斷一律失敗
|
|
361
|
+
* @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,4xx(429除外)與截斷不重試,預設0
|
|
293
362
|
* @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
|
|
363
|
+
* @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)、finishReason(已取得回應時之正規化終止原因字串,completed為'stop'、max_output_tokens為'length')、truncated(已取得回應時是否被截斷布林值,僅status為incomplete時為true),本函數不會reject
|
|
295
364
|
* @example
|
|
296
365
|
* //need network, no cli required
|
|
297
366
|
*
|
|
@@ -374,6 +443,9 @@ async function dispatchApiOpenaiResponses(prompt, opt = {}) {
|
|
|
374
443
|
//validator
|
|
375
444
|
let validator = buildValidator(get(opt, 'validate', null))
|
|
376
445
|
|
|
446
|
+
//acceptTruncated, 僅明示true才放行截斷內容(見檔頭)
|
|
447
|
+
let acceptTruncated = get(opt, 'acceptTruncated', null) === true
|
|
448
|
+
|
|
377
449
|
//url, baseURL尾端斜線正規化後接上端點
|
|
378
450
|
let url = baseURL.replace(/\/+$/, '') + '/responses'
|
|
379
451
|
|
|
@@ -384,8 +456,8 @@ async function dispatchApiOpenaiResponses(prompt, opt = {}) {
|
|
|
384
456
|
}
|
|
385
457
|
body = { ...body, ...bodyExtra }
|
|
386
458
|
|
|
387
|
-
//headers
|
|
388
|
-
let headers = { 'Content-Type': 'application/json', ...headersExtra }
|
|
459
|
+
//headers, Accept-Encoding預設identity(防伺服器壓縮卻漏標Content-Encoding, 見檔頭), headersExtra同名鍵可覆寫
|
|
460
|
+
let headers = { 'Content-Type': 'application/json', 'Accept-Encoding': 'identity', ...headersExtra }
|
|
389
461
|
if (isestr(key)) {
|
|
390
462
|
headers['Authorization'] = `Bearer ${key}`
|
|
391
463
|
}
|
|
@@ -400,7 +472,7 @@ async function dispatchApiOpenaiResponses(prompt, opt = {}) {
|
|
|
400
472
|
await delay(Math.min(retryDelayMs * attempt, MAX_RETRY_DELAY_MS))
|
|
401
473
|
}
|
|
402
474
|
|
|
403
|
-
lastResult = await callOnce(url, headers, body, timeoutMs, validator)
|
|
475
|
+
lastResult = await callOnce(url, headers, body, timeoutMs, validator, acceptTruncated)
|
|
404
476
|
totalAttempts = attempt + 1
|
|
405
477
|
|
|
406
478
|
if (lastResult.ok) {
|
|
@@ -408,6 +480,11 @@ async function dispatchApiOpenaiResponses(prompt, opt = {}) {
|
|
|
408
480
|
return lastResult
|
|
409
481
|
}
|
|
410
482
|
|
|
483
|
+
//不可重試: 截斷為決定性(同一請求必然再截斷, 重試只會再燒一次輸出上限)
|
|
484
|
+
if (lastResult.truncated === true) {
|
|
485
|
+
break
|
|
486
|
+
}
|
|
487
|
+
|
|
411
488
|
//不可重試: 4xx(429除外)為客戶端錯誤, 重試無意義
|
|
412
489
|
let c = lastResult.code
|
|
413
490
|
if (isnum(c) && c >= 400 && c < 500 && c !== 429) {
|
|
@@ -11,6 +11,7 @@ import castPintOr from './castPintOr.mjs'
|
|
|
11
11
|
import buildValidator from './buildValidator.mjs'
|
|
12
12
|
import getErrorResult from './getErrorResult.mjs'
|
|
13
13
|
import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
14
|
+
import { safeValidate } from './checkTruncation.mjs'
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
// dispatchApiTypesafeSystemone.mjs — 以fetch直呼TypeSafe AI之System One API(POST /v1/systemone)
|
|
@@ -41,6 +42,9 @@ import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
|
41
42
|
// {detail:{error_type:'api_usage_error', message:'Unknown model: X'}}; 題型不合規或questions為空422
|
|
42
43
|
// {detail:[{type,loc,msg,...}]}。皆以HTTP <code>回報、原始本體置stderr; 4xx(429除外)不重試。
|
|
43
44
|
//
|
|
45
|
+
// 【預設帶Accept-Encoding: identity(2026-09-24起)】防伺服器壓縮卻漏標Content-Encoding而令fetch不解壓、
|
|
46
|
+
// JSON.parse失敗; 與dispatchApiOpenaiCompat同一決策與依據(見該檔檔頭; 本kind亦經Zen之/systemone), opt.headers可覆寫。
|
|
47
|
+
//
|
|
44
48
|
// 【混用注意】
|
|
45
49
|
// 1. 答案形狀與文字模型完全不同, 不可與文字生成條目混在同一條dispatchAiFallback鏈中遞補;
|
|
46
50
|
// 同一鏈只放本kind之條目(可多把金鑰輪替)。預設providers.mjs收有typesafe:jev-latest,
|
|
@@ -180,16 +184,20 @@ async function callOnce(url, headers, body, ids, timeoutMs, validator) {
|
|
|
180
184
|
//content, answers序列化為stdout(令遞補層與工作流層之parse/check通用)
|
|
181
185
|
let content = JSON.stringify(answers)
|
|
182
186
|
|
|
183
|
-
//validator, error與execCli一致令dispatchAiFallback
|
|
184
|
-
if (validator
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
187
|
+
//validator, error與execCli一致令dispatchAiFallback可統一分流; 拋錯視同拒絕(不reject), 訊息置stderr
|
|
188
|
+
if (validator) {
|
|
189
|
+
let v = safeValidate(validator, content)
|
|
190
|
+
if (!v.pass) {
|
|
191
|
+
return mkResult({
|
|
192
|
+
stdout: strTruncate(content, 500, optTruncate),
|
|
193
|
+
stderr: v.threw ? `validate threw: ${v.threw}` : '',
|
|
194
|
+
code: res.status,
|
|
195
|
+
error: 'OUTPUT_VALIDATION_FAILED',
|
|
196
|
+
errorType: 'validation',
|
|
197
|
+
usage,
|
|
198
|
+
modelResolved,
|
|
199
|
+
})
|
|
200
|
+
}
|
|
193
201
|
}
|
|
194
202
|
|
|
195
203
|
return mkResult({
|
|
@@ -225,9 +233,9 @@ async function callOnce(url, headers, body, ids, timeoutMs, validator) {
|
|
|
225
233
|
* @param {String} [opt.model='jev-latest'] 輸入模型名稱字串,預設'jev-latest'(另有'jev-preview')
|
|
226
234
|
* @param {String} [opt.key=''] 輸入API key字串,以Bearer置於Authorization標頭,預設''代表不帶認證標頭
|
|
227
235
|
* @param {Object} [opt.body={}] 輸入額外請求本體物件,將併入預設body(同名鍵以此為準),預設{}
|
|
228
|
-
* @param {Object} [opt.headers={}]
|
|
236
|
+
* @param {Object} [opt.headers={}] 輸入額外請求標頭物件,同名鍵覆寫預設標頭;預設標頭含'Accept-Encoding: identity'(防伺服器壓縮卻漏標Content-Encoding,見檔頭),預設{}
|
|
229
237
|
* @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將中止請求,全套件統一預設300000
|
|
230
|
-
* @param {String|Function} [opt.validate=undefined] 輸入stdout(answers之JSON字串)驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100'
|
|
238
|
+
* @param {String|Function} [opt.validate=undefined] 輸入stdout(answers之JSON字串)驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,自訂函數拋錯視同驗證失敗,預設undefined代表不驗證
|
|
231
239
|
* @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,4xx(429除外)不重試,預設0
|
|
232
240
|
* @param {Number} [opt.retryDelayMs=5000] 輸入重試間隔毫秒正整數,實際間隔為retryDelayMs乘以重試次數且上限15000ms,預設5000
|
|
233
241
|
* @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(answers之JSON字串)、stderr(失敗時之原始回應本體)、code(HTTP狀態碼,網路錯誤與逾時為null)、error(錯誤訊息字串,成功時為空字串)、errorType(僅失敗時,機器可讀錯誤類別字串,一覽見getErrorType.mjs檔頭)、durationMs(耗時毫秒)、attempts(實際嘗試次數)、usage(原始回應之token用量物件原樣透傳,欄位名為input_tokens/output_tokens,無則null)、answers(成功時為已解析之答案物件,否則null)、modelResolved(回應所載之實際模型版本字串,如'jev-1.13.0',無則''),本函數不會reject
|
|
@@ -335,7 +343,7 @@ async function dispatchApiTypesafeSystemone(prompt, opt = {}) {
|
|
|
335
343
|
let ids = Object.keys(isobj(body.questions) ? body.questions : questions)
|
|
336
344
|
|
|
337
345
|
//headers
|
|
338
|
-
let headers = { 'Content-Type': 'application/json', ...headersExtra }
|
|
346
|
+
let headers = { 'Content-Type': 'application/json', 'Accept-Encoding': 'identity', ...headersExtra } //Accept-Encoding預設identity, 見檔頭
|
|
339
347
|
if (isestr(key)) {
|
|
340
348
|
headers['Authorization'] = `Bearer ${key}`
|
|
341
349
|
}
|
package/src/getErrorType.mjs
CHANGED
|
@@ -24,8 +24,10 @@ import isestr from 'wsemi/src/isestr.mjs'
|
|
|
24
24
|
// 'tool-unsupported' 模型回tool_calls而本轉接器不支援工具(僅api類)
|
|
25
25
|
// 'invalid-response' 回應結構不合規(缺choices[0].message.content、缺output陣列,
|
|
26
26
|
// 或api-typesafe-systemone缺answers物件/缺所請求題目之答案, 僅api類)
|
|
27
|
-
// 'incomplete'
|
|
28
|
-
//
|
|
27
|
+
// 'incomplete' 回應未完整: 截斷(chat/completions之finish_reason為length/content_filter、
|
|
28
|
+
// Responses API之status為incomplete; 結果另帶truncated:true, 規則見checkTruncation.mjs)
|
|
29
|
+
// 與Responses API之其餘非completed狀態(如failed; truncated:false)。僅api類;
|
|
30
|
+
// 半截內容預設不當成功回傳, 呼叫端明示acceptTruncated才可放行(2026-09-24起兩轉接器同規則)
|
|
29
31
|
// 'aborted' shouldStop中止(僅dispatchAiFallback)
|
|
30
32
|
// 'budget' 時間預算用盡(僅dispatchAiFallback)
|
|
31
33
|
|