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.
- package/README.md +26 -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/describeNonJsonBody.mjs.html +122 -0
- package/docs/dfTimeoutMs.mjs.html +2 -2
- package/docs/dispatchAi.mjs.html +2 -2
- package/docs/dispatchAiFallback.mjs.html +42 -8
- package/docs/dispatchAiWkf.mjs.html +2 -2
- package/docs/dispatchAntigravity.mjs.html +2 -2
- package/docs/dispatchApiOpenaiCompat.mjs.html +124 -31
- package/docs/dispatchApiOpenaiResponses.mjs.html +113 -27
- package/docs/dispatchApiTypesafeSystemone.mjs.html +42 -17
- 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 +9 -5
- package/docs/global.html +3159 -1806
- 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 +2 -2
- package/docs/wkf_callAiWithFallback.mjs.html +64 -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 +13 -2
- package/package.json +1 -1
- package/src/checkTruncation.mjs +127 -0
- package/src/describeNonJsonBody.mjs +50 -0
- package/src/dispatchAiFallback.mjs +40 -6
- package/src/dispatchApiOpenaiCompat.mjs +122 -29
- package/src/dispatchApiOpenaiResponses.mjs +111 -25
- package/src/dispatchApiTypesafeSystemone.mjs +40 -15
- package/src/getErrorType.mjs +7 -3
- package/src/providers.mjs +1 -1
- package/src/wkf/callAiWithFallback.mjs +62 -18
- package/src/wkf/runFanout.mjs +2 -0
- package/src/wkf/salvageTruncatedArray.mjs +11 -0
- package/test/tools/fakeServerForApiTest.mjs +87 -2
- package/test/unit-dispatchApiOpenaiCompat.test.mjs +207 -1
- package/test/unit-dispatchApiOpenaiResponses.test.mjs +74 -5
- package/test/unit-dispatchApiTypesafeSystemone.test.mjs +25 -1
|
@@ -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#NO_SIDE_EFFECT">NO_SIDE_EFFECT</a></li><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#agyPrint">agyPrint</a></li><li><a href="global.html#attachErrorType">attachErrorType</a></li><li><a href="global.html#budgetFor">budgetFor</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#castPintOr">castPintOr</a></li><li><a href="global.html#collectAdditional">collectAdditional</a></li><li><a href="global.html#createFileStore">createFileStore</a></li><li><a href="global.html#createUsageCounter">createUsageCounter</a></li><li><a href="global.html#decodeJwtPayload">decodeJwtPayload</a></li><li><a href="global.html#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dfQuotaTimeoutMs">dfQuotaTimeoutMs</a></li><li><a href="global.html#dfTimeoutMs">dfTimeoutMs</a></li><li><a href="global.html#directWindow">directWindow</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#dispatchApiOpenaiResponses">dispatchApiOpenaiResponses</a></li><li><a href="global.html#dispatchApiTypesafeSystemone">dispatchApiTypesafeSystemone</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#extractOutputText">extractOutputText</a></li><li><a href="global.html#fetchQuotaJson">fetchQuotaJson</a></li><li><a href="global.html#firstStr">firstStr</a></li><li><a href="global.html#fromBucket">fromBucket</a></li><li><a href="global.html#fromCodexUsageHttp">fromCodexUsageHttp</a></li><li><a href="global.html#fromLimitItem">fromLimitItem</a></li><li><a href="global.html#fromRpcWindow">fromRpcWindow</a></li><li><a href="global.html#fromTopField">fromTopField</a></li><li><a href="global.html#fromWindow">fromWindow</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#getErrorType">getErrorType</a></li><li><a href="global.html#getQuotaAntigravity">getQuotaAntigravity</a></li><li><a href="global.html#getQuotaClaude">getQuotaClaude</a></li><li><a href="global.html#getQuotaCodex">getQuotaCodex</a></li><li><a href="global.html#initState">initState</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li><li><a href="global.html#pickWindow">pickWindow</a></li><li><a href="global.html#readBodyCapped">readBodyCapped</a></li><li><a href="global.html#readEnvFile">readEnvFile</a></li><li><a href="global.html#readJsonOrNull">readJsonOrNull</a></li><li><a href="global.html#redactText">redactText</a></li><li><a href="global.html#reorderByCooling">reorderByCooling</a></li><li><a href="global.html#resolveProviders">resolveProviders</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><li><a href="global.html#salvageTruncatedArray">salvageTruncatedArray</a></li><li><a href="global.html#toQuotaLabel">toQuotaLabel</a></li><li><a href="global.html#toQuotaResult">toQuotaResult</a></li><li><a href="global.html#toQuotaScopedLabel">toQuotaScopedLabel</a></li><li><a href="global.html#toQuotaWindow">toQuotaWindow</a></li><li><a href="global.html#viaAppServer">viaAppServer</a></li><li><a href="global.html#viaHttp">viaHttp</a></li><li><a href="global.html#windowToSeconds">windowToSeconds</a></li></ul>
|
|
32
|
+
<h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#NO_SIDE_EFFECT">NO_SIDE_EFFECT</a></li><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#agyPrint">agyPrint</a></li><li><a href="global.html#attachErrorType">attachErrorType</a></li><li><a href="global.html#budgetFor">budgetFor</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#castPintOr">castPintOr</a></li><li><a href="global.html#collectAdditional">collectAdditional</a></li><li><a href="global.html#createFileStore">createFileStore</a></li><li><a href="global.html#createUsageCounter">createUsageCounter</a></li><li><a href="global.html#decodeJwtPayload">decodeJwtPayload</a></li><li><a href="global.html#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#describeNonJsonBody">describeNonJsonBody</a></li><li><a href="global.html#dfQuotaTimeoutMs">dfQuotaTimeoutMs</a></li><li><a href="global.html#dfTimeoutMs">dfTimeoutMs</a></li><li><a href="global.html#directWindow">directWindow</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#dispatchApiOpenaiResponses">dispatchApiOpenaiResponses</a></li><li><a href="global.html#dispatchApiTypesafeSystemone">dispatchApiTypesafeSystemone</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#extractOutputText">extractOutputText</a></li><li><a href="global.html#fetchQuotaJson">fetchQuotaJson</a></li><li><a href="global.html#finOf">finOf</a></li><li><a href="global.html#firstStr">firstStr</a></li><li><a href="global.html#fromBucket">fromBucket</a></li><li><a href="global.html#fromCodexUsageHttp">fromCodexUsageHttp</a></li><li><a href="global.html#fromLimitItem">fromLimitItem</a></li><li><a href="global.html#fromRpcWindow">fromRpcWindow</a></li><li><a href="global.html#fromTopField">fromTopField</a></li><li><a href="global.html#fromWindow">fromWindow</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#getErrorType">getErrorType</a></li><li><a href="global.html#getQuotaAntigravity">getQuotaAntigravity</a></li><li><a href="global.html#getQuotaClaude">getQuotaClaude</a></li><li><a href="global.html#getQuotaCodex">getQuotaCodex</a></li><li><a href="global.html#initState">initState</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li><li><a href="global.html#judgeTruncated">judgeTruncated</a></li><li><a href="global.html#normalizeFinishReason">normalizeFinishReason</a></li><li><a href="global.html#pickWindow">pickWindow</a></li><li><a href="global.html#readBodyCapped">readBodyCapped</a></li><li><a href="global.html#readEnvFile">readEnvFile</a></li><li><a href="global.html#readJsonOrNull">readJsonOrNull</a></li><li><a href="global.html#redactText">redactText</a></li><li><a href="global.html#reorderByCooling">reorderByCooling</a></li><li><a href="global.html#resolveProviders">resolveProviders</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><li><a href="global.html#safeValidate">safeValidate</a></li><li><a href="global.html#salvageTruncatedArray">salvageTruncatedArray</a></li><li><a href="global.html#toQuotaLabel">toQuotaLabel</a></li><li><a href="global.html#toQuotaResult">toQuotaResult</a></li><li><a href="global.html#toQuotaScopedLabel">toQuotaScopedLabel</a></li><li><a href="global.html#toQuotaWindow">toQuotaWindow</a></li><li><a href="global.html#viaAppServer">viaAppServer</a></li><li><a href="global.html#viaHttp">viaHttp</a></li><li><a href="global.html#windowToSeconds">windowToSeconds</a></li></ul>
|
|
33
33
|
|
|
34
34
|
</nav>
|
|
35
35
|
|
|
@@ -57,6 +57,8 @@ import buildValidator from './buildValidator.mjs'
|
|
|
57
57
|
import strTruncate from 'wsemi/src/strTruncate.mjs'
|
|
58
58
|
import getErrorResult from './getErrorResult.mjs'
|
|
59
59
|
import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
60
|
+
import { TRUNCATION_REASONS, normalizeFinishReason, safeValidate, judgeTruncated } from './checkTruncation.mjs'
|
|
61
|
+
import describeNonJsonBody from './describeNonJsonBody.mjs'
|
|
60
62
|
|
|
61
63
|
|
|
62
64
|
// dispatchApiOpenaiCompat.mjs — 以fetch直呼OpenAI相容API(chat/completions)
|
|
@@ -86,17 +88,39 @@ import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
|
86
88
|
// 故呼叫端若於body帶入tools, 本函數一律以TOOL_CALLS_UNSUPPORTED回報失敗而不假裝成功
|
|
87
89
|
// (實測Agnes於tool_calls時content為"\n\n"而非null, 不特別處理會靜默回傳空白內容)。
|
|
88
90
|
//
|
|
89
|
-
//
|
|
91
|
+
// 【預設帶Accept-Encoding: identity(2026-09-24起, 三個REST轉接器同步)】Node內建fetch(undici)只在回應
|
|
92
|
+
// 帶Content-Encoding時才自動解壓; 伺服器若壓縮了本體卻漏標此標頭, fetch原樣交出壓縮位元組,
|
|
93
|
+
// JSON.parse失敗而回INVALID_RESPONSE(使用端回報Zen之space-bunny-free即此症: 手動brotli解壓得完整答案,
|
|
94
|
+
// 改帶identity即得正常JSON)。本機以假伺服器重現該機制。重現條件(安裝方tai-kns-trade, 2026-09-24 11:5x實測):
|
|
95
|
+
// 長回應(chat/completions約35秒)時Node fetch所見回應標頭為0個、本體9,224 bytes為brotli位元組, 改帶identity
|
|
96
|
+
// 則本體為22,124 bytes之JSON; 同環境同時段他端點標頭正常(npm registry 13個、Zen /models 8個含content-encoding=br)
|
|
97
|
+
// 且未設代理。本機同日對Zen取樣7次(含65KB長回應)皆正確標示br、未重現——推測漏標為有條件出現(長回應),
|
|
98
|
+
// 條件未定。identity請伺服器勿壓縮, 從源頭消除「壓縮處理不一致」一類失敗(不論成因在伺服器、代理或執行環境),
|
|
99
|
+
// 代價僅傳輸量變大(實測Zen回應約5~8KB→20~65KB)。呼叫端可以opt.headers之'Accept-Encoding'覆寫。
|
|
100
|
+
//
|
|
101
|
+
// 【HTTP 200但本體非JSON另報(安裝方建議C2)】JSON.parse失敗時不再與「缺choices[0].message.content」同一句,
|
|
102
|
+
// 改回INVALID_RESPONSE: body is not JSON(附原始位元組數、前16位元組hex、content-encoding、content-type,
|
|
103
|
+
// 規則單一來源見describeNonJsonBody.mjs), 遞補層據此整組跳過; 「JSON缺content」維持換金鑰。
|
|
104
|
+
//
|
|
105
|
+
// 【截斷(finish_reason為length或content_filter)預設失敗(2026-09-24起, 規則單一來源見checkTruncation.mjs)】
|
|
106
|
+
// 舊版不看finish_reason, 實測Zen之space-bunny-free於max_tokens:600時推理即耗盡而回content:""、
|
|
107
|
+
// 本轉接器卻回ok:true(靜默成功)。現於null檢查與validate之前裁定: 預設回INCOMPLETE_RESPONSE
|
|
108
|
+
// (errorType incomplete, 結果帶truncated:true), 不論validate為何——「validate接受」不代表內容完整。
|
|
109
|
+
// 呼叫端明示acceptTruncated:true才放行length之截斷(交validate裁決, 通過者仍標truncated:true);
|
|
110
|
+
// content_filter與可見輸出為空者一律失敗。finish_reason為stop/null/未知值者照舊(不可誤殺)。
|
|
111
|
+
//
|
|
112
|
+
// 【重試語意對齊execCli】4xx(429除外)為客戶端錯誤不可重試而立即中止; 截斷亦不重試(同一請求必然再截斷);
|
|
90
113
|
// 429/5xx/網路錯誤/逾時依maxRetries線性退避重試(間隔retryDelayMs*次數, 上限15000ms)。
|
|
91
114
|
//
|
|
92
115
|
// 【結果結構對齊execCli】{ ok, stdout, stderr, code, error, durationMs, attempts, usage },
|
|
93
116
|
// usage為原始回應之token用量原樣透傳(無則null; 驗證失敗等已耗token之失敗亦帶出),
|
|
94
117
|
// CLI類轉接器無可靠來源故無此欄——呼叫端可據此把「真實用量」與「只能估算」分開處理。
|
|
118
|
+
// 取得並解析回應後之結果另帶finishReason(正規化終止原因, 缺值為'')與truncated(是否截斷)。
|
|
95
119
|
// 失敗結果另帶機器可讀之errorType(timeout/fetch/http/tool-unsupported/invalid-response/
|
|
96
|
-
// validation/params, 一覽見getErrorType.mjs檔頭), error字串保留不動, 兩者並存。
|
|
120
|
+
// incomplete/validation/params, 一覽見getErrorType.mjs檔頭), error字串保留不動, 兩者並存。
|
|
97
121
|
// stdout為回覆內容、code為HTTP狀態碼(網路錯誤與逾時為null)、逾時error以TIMEOUT開頭、
|
|
98
|
-
// 驗證失敗error為OUTPUT_VALIDATION_FAILED
|
|
99
|
-
//
|
|
122
|
+
// 驗證失敗error為OUTPUT_VALIDATION_FAILED(validate拋錯亦同, 拋錯訊息置stderr)——
|
|
123
|
+
// dispatchAiFallback據此分流: 逾時/驗證失敗/截斷/工具不支援/本體非JSON整組跳過, 其餘換金鑰。
|
|
100
124
|
|
|
101
125
|
|
|
102
126
|
//預設值
|
|
@@ -119,9 +143,10 @@ let optTruncate = {
|
|
|
119
143
|
* @param {Object} body 輸入請求本體物件
|
|
120
144
|
* @param {Number} timeoutMs 輸入逾時毫秒
|
|
121
145
|
* @param {Function|null} validator 輸入驗證函式
|
|
146
|
+
* @param {Boolean} acceptTruncated 輸入是否明示接受截斷內容
|
|
122
147
|
* @returns {Promise} 回傳Promise,resolve回傳結果物件
|
|
123
148
|
*/
|
|
124
|
-
async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
149
|
+
async function callOnce(url, headers, body, timeoutMs, validator, acceptTruncated) {
|
|
125
150
|
|
|
126
151
|
let t0 = Date.now()
|
|
127
152
|
|
|
@@ -144,7 +169,9 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
144
169
|
controller.abort()
|
|
145
170
|
}, timeoutMs)
|
|
146
171
|
|
|
172
|
+
//本體先取原始位元組再以UTF-8解碼(等同res.text()), 本體非JSON時才有原始位元組可供診斷(見describeNonJsonBody.mjs)
|
|
147
173
|
let res = null
|
|
174
|
+
let raw = new Uint8Array(0)
|
|
148
175
|
let txt = ''
|
|
149
176
|
try {
|
|
150
177
|
res = await fetch(url, {
|
|
@@ -153,7 +180,8 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
153
180
|
body: JSON.stringify(body),
|
|
154
181
|
signal: controller.signal,
|
|
155
182
|
})
|
|
156
|
-
|
|
183
|
+
raw = new Uint8Array(await res.arrayBuffer())
|
|
184
|
+
txt = new TextDecoder('utf-8').decode(raw)
|
|
157
185
|
}
|
|
158
186
|
catch (err) {
|
|
159
187
|
clearTimeout(timer)
|
|
@@ -181,20 +209,39 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
181
209
|
|
|
182
210
|
//取出choices[0]與usage(token用量, 原樣透傳; 失敗回應亦可能已耗token, 一併帶出)
|
|
183
211
|
let content = null
|
|
184
|
-
let
|
|
212
|
+
let finishReasonRaw = null
|
|
185
213
|
let toolCalls = null
|
|
186
214
|
let usage = null
|
|
215
|
+
let parsed = true
|
|
187
216
|
try {
|
|
188
217
|
let j = JSON.parse(txt)
|
|
189
218
|
content = get(j, 'choices.0.message.content', null)
|
|
190
|
-
|
|
219
|
+
finishReasonRaw = get(j, 'choices.0.finish_reason', null)
|
|
191
220
|
toolCalls = get(j, 'choices.0.message.tool_calls', null)
|
|
192
221
|
usage = get(j, 'usage', null)
|
|
193
222
|
if (!isobj(usage)) {
|
|
194
223
|
usage = null
|
|
195
224
|
}
|
|
196
225
|
}
|
|
197
|
-
catch {
|
|
226
|
+
catch {
|
|
227
|
+
parsed = false
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
//本體非JSON, 與「JSON缺content」分開回報並附原始位元組資訊; 遞補層據前綴整組跳過(見describeNonJsonBody.mjs)
|
|
231
|
+
if (!parsed) {
|
|
232
|
+
return mkResult({
|
|
233
|
+
stderr: strTruncate(txt, 500, optTruncate),
|
|
234
|
+
code: res.status,
|
|
235
|
+
error: describeNonJsonBody(raw, res.headers),
|
|
236
|
+
errorType: 'invalid-response',
|
|
237
|
+
finishReason: '',
|
|
238
|
+
truncated: false,
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
//fin, 自此起之每個結果皆外顯正規化終止原因與是否截斷(規則見checkTruncation.mjs)
|
|
243
|
+
let finishReason = normalizeFinishReason(finishReasonRaw)
|
|
244
|
+
let fin = { finishReason, truncated: TRUNCATION_REASONS.includes(finishReason) }
|
|
198
245
|
|
|
199
246
|
//tool_calls, 本轉接器不支援工具迴圈(見檔頭), 明確回報而不假裝成功
|
|
200
247
|
//(Agnes於tool_calls時content為"\n\n"非null, 不攔截會靜默回傳空白內容)
|
|
@@ -205,31 +252,67 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
205
252
|
error: 'TOOL_CALLS_UNSUPPORTED: use a cli kind (opencode/claude/codex/antigravity) when tools are needed',
|
|
206
253
|
errorType: 'tool-unsupported',
|
|
207
254
|
usage,
|
|
255
|
+
...fin,
|
|
208
256
|
})
|
|
209
257
|
}
|
|
210
258
|
|
|
211
|
-
|
|
259
|
+
//content字串化(少數閘道回array形態); null/undefined一律視為無內容
|
|
260
|
+
if (content === undefined) {
|
|
261
|
+
content = null
|
|
262
|
+
}
|
|
263
|
+
if (content !== null && typeof content !== 'string') {
|
|
264
|
+
content = JSON.stringify(content)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
//截斷, 於null檢查與validate之前裁定: 預設失敗, 明示acceptTruncated才交validate放行(見檔頭)
|
|
268
|
+
if (fin.truncated) {
|
|
269
|
+
let d = judgeTruncated({
|
|
270
|
+
finishReason,
|
|
271
|
+
content,
|
|
272
|
+
acceptTruncated,
|
|
273
|
+
validator,
|
|
274
|
+
reasoningTokens: get(usage, 'completion_tokens_details.reasoning_tokens', null),
|
|
275
|
+
label: `finish_reason=${finishReason}`,
|
|
276
|
+
})
|
|
277
|
+
if (!d.accept) {
|
|
278
|
+
return mkResult({
|
|
279
|
+
stdout: strTruncate(content || '', 500, optTruncate),
|
|
280
|
+
stderr: strTruncate((d.threw ? `validate threw: ${d.threw}\n` : '') + txt, 500, optTruncate),
|
|
281
|
+
code: res.status,
|
|
282
|
+
error: d.error,
|
|
283
|
+
errorType: 'incomplete',
|
|
284
|
+
usage,
|
|
285
|
+
...fin,
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
return mkResult({ ok: true, stdout: content, code: res.status, usage, ...fin })
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (content === null) {
|
|
212
292
|
return mkResult({
|
|
213
293
|
stderr: strTruncate(txt, 500, optTruncate),
|
|
214
294
|
code: res.status,
|
|
215
295
|
error: 'INVALID_RESPONSE: missing choices[0].message.content',
|
|
216
296
|
errorType: 'invalid-response',
|
|
217
297
|
usage,
|
|
298
|
+
...fin,
|
|
218
299
|
})
|
|
219
300
|
}
|
|
220
|
-
if (typeof content !== 'string') {
|
|
221
|
-
content = JSON.stringify(content) //少數閘道回array形態
|
|
222
|
-
}
|
|
223
301
|
|
|
224
|
-
//validator, error與execCli一致令dispatchAiFallback
|
|
225
|
-
if (validator
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
302
|
+
//validator, error與execCli一致令dispatchAiFallback可統一分流; 拋錯視同拒絕(不reject), 訊息置stderr
|
|
303
|
+
if (validator) {
|
|
304
|
+
let v = safeValidate(validator, content)
|
|
305
|
+
if (!v.pass) {
|
|
306
|
+
return mkResult({
|
|
307
|
+
stdout: strTruncate(content, 500, optTruncate),
|
|
308
|
+
stderr: v.threw ? `validate threw: ${v.threw}` : '',
|
|
309
|
+
code: res.status,
|
|
310
|
+
error: 'OUTPUT_VALIDATION_FAILED',
|
|
311
|
+
errorType: 'validation',
|
|
312
|
+
usage,
|
|
313
|
+
...fin,
|
|
314
|
+
})
|
|
315
|
+
}
|
|
233
316
|
}
|
|
234
317
|
|
|
235
318
|
return mkResult({
|
|
@@ -237,6 +320,7 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
237
320
|
stdout: content,
|
|
238
321
|
code: res.status,
|
|
239
322
|
usage,
|
|
323
|
+
...fin,
|
|
240
324
|
})
|
|
241
325
|
}
|
|
242
326
|
|
|
@@ -263,12 +347,13 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
263
347
|
* @param {String} [opt.key=''] 輸入API key字串,以Bearer置於Authorization標頭,預設''代表不帶認證標頭
|
|
264
348
|
* @param {String} [opt.system=''] 輸入system提示詞字串,將以system角色置於messages首位,預設''代表不帶
|
|
265
349
|
* @param {Object} [opt.body={}] 輸入額外請求本體物件(如temperature、max_tokens、response_format),將併入預設body(同名鍵以此為準),預設{}。注意本轉接器不支援工具,帶入tools而模型回tool_calls時一律以TOOL_CALLS_UNSUPPORTED回報失敗,需要工具請改用CLI類kind
|
|
266
|
-
* @param {Object} [opt.headers={}]
|
|
350
|
+
* @param {Object} [opt.headers={}] 輸入額外請求標頭物件,同名鍵覆寫預設標頭;預設標頭含'Accept-Encoding: identity'(防伺服器壓縮卻漏標Content-Encoding,見檔頭),要改回允許壓縮可給{'Accept-Encoding':'gzip, deflate, br'},預設{}
|
|
267
351
|
* @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將中止請求(含回應串流讀取),全套件統一預設300000
|
|
268
|
-
* @param {String|Function} [opt.validate=undefined] 輸入回覆內容驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100'
|
|
269
|
-
* @param {
|
|
352
|
+
* @param {String|Function} [opt.validate=undefined] 輸入回覆內容驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,自訂函數拋錯視同驗證失敗,預設undefined代表不驗證
|
|
353
|
+
* @param {Boolean} [opt.acceptTruncated=false] 輸入是否接受被截斷(finish_reason為length)之內容布林值,true代表交validate裁決(無validate則直接接受)且結果標truncated:true;content_filter與可見輸出為空者一律失敗,預設false代表截斷一律失敗(errorType incomplete)
|
|
354
|
+
* @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,4xx(429除外)與截斷不重試,預設0
|
|
270
355
|
* @param {Number} [opt.retryDelayMs=5000] 輸入重試間隔毫秒正整數,實際間隔為retryDelayMs乘以重試次數且上限15000ms,預設5000
|
|
271
|
-
* @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(回覆內容字串)、stderr(失敗時之原始回應本體)、code(HTTP狀態碼,網路錯誤與逾時為null)、error(錯誤訊息字串,成功時為空字串)、errorType(僅失敗時,機器可讀錯誤類別字串,一覽見getErrorType.mjs檔頭)、durationMs(耗時毫秒)、attempts(實際嘗試次數)、usage(原始回應之token用量物件原樣透傳,無則null),本函數不會reject
|
|
356
|
+
* @returns {Promise} 回傳Promise,resolve回傳結果物件,內含ok(是否成功布林值)、stdout(回覆內容字串)、stderr(失敗時之原始回應本體)、code(HTTP狀態碼,網路錯誤與逾時為null)、error(錯誤訊息字串,成功時為空字串)、errorType(僅失敗時,機器可讀錯誤類別字串,一覽見getErrorType.mjs檔頭)、durationMs(耗時毫秒)、attempts(實際嘗試次數)、usage(原始回應之token用量物件原樣透傳,無則null)、finishReason(已取得回應時之正規化終止原因字串,缺值為'')、truncated(已取得回應時是否被截斷布林值),本函數不會reject
|
|
272
357
|
* @example
|
|
273
358
|
* //need network, no cli required
|
|
274
359
|
*
|
|
@@ -362,6 +447,9 @@ async function dispatchApiOpenaiCompat(prompt, opt = {}) {
|
|
|
362
447
|
//validator
|
|
363
448
|
let validator = buildValidator(get(opt, 'validate', null))
|
|
364
449
|
|
|
450
|
+
//acceptTruncated, 僅明示true才放行截斷內容(見檔頭)
|
|
451
|
+
let acceptTruncated = get(opt, 'acceptTruncated', null) === true
|
|
452
|
+
|
|
365
453
|
//url, baseURL尾端斜線正規化後接上端點
|
|
366
454
|
let url = baseURL.replace(/\/+$/, '') + '/chat/completions'
|
|
367
455
|
|
|
@@ -375,8 +463,8 @@ async function dispatchApiOpenaiCompat(prompt, opt = {}) {
|
|
|
375
463
|
//body, 額外鍵以bodyExtra為準(可覆寫temperature等, 覆寫messages屬進階用法)
|
|
376
464
|
let body = { model, messages, ...bodyExtra }
|
|
377
465
|
|
|
378
|
-
//headers
|
|
379
|
-
let headers = { 'Content-Type': 'application/json', ...headersExtra }
|
|
466
|
+
//headers, Accept-Encoding預設identity(防伺服器壓縮卻漏標Content-Encoding, 見檔頭), headersExtra同名鍵可覆寫
|
|
467
|
+
let headers = { 'Content-Type': 'application/json', 'Accept-Encoding': 'identity', ...headersExtra }
|
|
380
468
|
if (isestr(key)) {
|
|
381
469
|
headers['Authorization'] = `Bearer ${key}`
|
|
382
470
|
}
|
|
@@ -391,7 +479,7 @@ async function dispatchApiOpenaiCompat(prompt, opt = {}) {
|
|
|
391
479
|
await delay(Math.min(retryDelayMs * attempt, MAX_RETRY_DELAY_MS))
|
|
392
480
|
}
|
|
393
481
|
|
|
394
|
-
lastResult = await callOnce(url, headers, body, timeoutMs, validator)
|
|
482
|
+
lastResult = await callOnce(url, headers, body, timeoutMs, validator, acceptTruncated)
|
|
395
483
|
totalAttempts = attempt + 1
|
|
396
484
|
|
|
397
485
|
if (lastResult.ok) {
|
|
@@ -399,6 +487,11 @@ async function dispatchApiOpenaiCompat(prompt, opt = {}) {
|
|
|
399
487
|
return lastResult
|
|
400
488
|
}
|
|
401
489
|
|
|
490
|
+
//不可重試: 截斷為決定性(同一請求必然再截斷, 重試只會再燒一次輸出上限)
|
|
491
|
+
if (lastResult.truncated === true) {
|
|
492
|
+
break
|
|
493
|
+
}
|
|
494
|
+
|
|
402
495
|
//不可重試: 4xx(429除外)為客戶端錯誤, 重試無意義
|
|
403
496
|
let c = lastResult.code
|
|
404
497
|
if (isnum(c) && c >= 400 && c < 500 && c !== 429) {
|
|
@@ -428,7 +521,7 @@ export default dispatchApiOpenaiCompat
|
|
|
428
521
|
<br class="clear">
|
|
429
522
|
|
|
430
523
|
<footer>
|
|
431
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Sep 24 2026
|
|
524
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Sep 24 2026 15:32:02 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
432
525
|
</footer>
|
|
433
526
|
|
|
434
527
|
<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#NO_SIDE_EFFECT">NO_SIDE_EFFECT</a></li><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#agyPrint">agyPrint</a></li><li><a href="global.html#attachErrorType">attachErrorType</a></li><li><a href="global.html#budgetFor">budgetFor</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#castPintOr">castPintOr</a></li><li><a href="global.html#collectAdditional">collectAdditional</a></li><li><a href="global.html#createFileStore">createFileStore</a></li><li><a href="global.html#createUsageCounter">createUsageCounter</a></li><li><a href="global.html#decodeJwtPayload">decodeJwtPayload</a></li><li><a href="global.html#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#dfQuotaTimeoutMs">dfQuotaTimeoutMs</a></li><li><a href="global.html#dfTimeoutMs">dfTimeoutMs</a></li><li><a href="global.html#directWindow">directWindow</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#dispatchApiOpenaiResponses">dispatchApiOpenaiResponses</a></li><li><a href="global.html#dispatchApiTypesafeSystemone">dispatchApiTypesafeSystemone</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#extractOutputText">extractOutputText</a></li><li><a href="global.html#fetchQuotaJson">fetchQuotaJson</a></li><li><a href="global.html#firstStr">firstStr</a></li><li><a href="global.html#fromBucket">fromBucket</a></li><li><a href="global.html#fromCodexUsageHttp">fromCodexUsageHttp</a></li><li><a href="global.html#fromLimitItem">fromLimitItem</a></li><li><a href="global.html#fromRpcWindow">fromRpcWindow</a></li><li><a href="global.html#fromTopField">fromTopField</a></li><li><a href="global.html#fromWindow">fromWindow</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#getErrorType">getErrorType</a></li><li><a href="global.html#getQuotaAntigravity">getQuotaAntigravity</a></li><li><a href="global.html#getQuotaClaude">getQuotaClaude</a></li><li><a href="global.html#getQuotaCodex">getQuotaCodex</a></li><li><a href="global.html#initState">initState</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li><li><a href="global.html#pickWindow">pickWindow</a></li><li><a href="global.html#readBodyCapped">readBodyCapped</a></li><li><a href="global.html#readEnvFile">readEnvFile</a></li><li><a href="global.html#readJsonOrNull">readJsonOrNull</a></li><li><a href="global.html#redactText">redactText</a></li><li><a href="global.html#reorderByCooling">reorderByCooling</a></li><li><a href="global.html#resolveProviders">resolveProviders</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><li><a href="global.html#salvageTruncatedArray">salvageTruncatedArray</a></li><li><a href="global.html#toQuotaLabel">toQuotaLabel</a></li><li><a href="global.html#toQuotaResult">toQuotaResult</a></li><li><a href="global.html#toQuotaScopedLabel">toQuotaScopedLabel</a></li><li><a href="global.html#toQuotaWindow">toQuotaWindow</a></li><li><a href="global.html#viaAppServer">viaAppServer</a></li><li><a href="global.html#viaHttp">viaHttp</a></li><li><a href="global.html#windowToSeconds">windowToSeconds</a></li></ul>
|
|
32
|
+
<h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#NO_SIDE_EFFECT">NO_SIDE_EFFECT</a></li><li><a href="global.html#WDispatchAi">WDispatchAi</a></li><li><a href="global.html#adapters">adapters</a></li><li><a href="global.html#agyPrint">agyPrint</a></li><li><a href="global.html#attachErrorType">attachErrorType</a></li><li><a href="global.html#budgetFor">budgetFor</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#castPintOr">castPintOr</a></li><li><a href="global.html#collectAdditional">collectAdditional</a></li><li><a href="global.html#createFileStore">createFileStore</a></li><li><a href="global.html#createUsageCounter">createUsageCounter</a></li><li><a href="global.html#decodeJwtPayload">decodeJwtPayload</a></li><li><a href="global.html#defaultIntegratePrompt">defaultIntegratePrompt</a></li><li><a href="global.html#describeNonJsonBody">describeNonJsonBody</a></li><li><a href="global.html#dfQuotaTimeoutMs">dfQuotaTimeoutMs</a></li><li><a href="global.html#dfTimeoutMs">dfTimeoutMs</a></li><li><a href="global.html#directWindow">directWindow</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#dispatchApiOpenaiResponses">dispatchApiOpenaiResponses</a></li><li><a href="global.html#dispatchApiTypesafeSystemone">dispatchApiTypesafeSystemone</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#extractOutputText">extractOutputText</a></li><li><a href="global.html#fetchQuotaJson">fetchQuotaJson</a></li><li><a href="global.html#finOf">finOf</a></li><li><a href="global.html#firstStr">firstStr</a></li><li><a href="global.html#fromBucket">fromBucket</a></li><li><a href="global.html#fromCodexUsageHttp">fromCodexUsageHttp</a></li><li><a href="global.html#fromLimitItem">fromLimitItem</a></li><li><a href="global.html#fromRpcWindow">fromRpcWindow</a></li><li><a href="global.html#fromTopField">fromTopField</a></li><li><a href="global.html#fromWindow">fromWindow</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#getErrorType">getErrorType</a></li><li><a href="global.html#getQuotaAntigravity">getQuotaAntigravity</a></li><li><a href="global.html#getQuotaClaude">getQuotaClaude</a></li><li><a href="global.html#getQuotaCodex">getQuotaCodex</a></li><li><a href="global.html#initState">initState</a></li><li><a href="global.html#isKeyIndependentFail">isKeyIndependentFail</a></li><li><a href="global.html#judgeTruncated">judgeTruncated</a></li><li><a href="global.html#normalizeFinishReason">normalizeFinishReason</a></li><li><a href="global.html#pickWindow">pickWindow</a></li><li><a href="global.html#readBodyCapped">readBodyCapped</a></li><li><a href="global.html#readEnvFile">readEnvFile</a></li><li><a href="global.html#readJsonOrNull">readJsonOrNull</a></li><li><a href="global.html#redactText">redactText</a></li><li><a href="global.html#reorderByCooling">reorderByCooling</a></li><li><a href="global.html#resolveProviders">resolveProviders</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><li><a href="global.html#safeValidate">safeValidate</a></li><li><a href="global.html#salvageTruncatedArray">salvageTruncatedArray</a></li><li><a href="global.html#toQuotaLabel">toQuotaLabel</a></li><li><a href="global.html#toQuotaResult">toQuotaResult</a></li><li><a href="global.html#toQuotaScopedLabel">toQuotaScopedLabel</a></li><li><a href="global.html#toQuotaWindow">toQuotaWindow</a></li><li><a href="global.html#viaAppServer">viaAppServer</a></li><li><a href="global.html#viaHttp">viaHttp</a></li><li><a href="global.html#windowToSeconds">windowToSeconds</a></li></ul>
|
|
33
33
|
|
|
34
34
|
</nav>
|
|
35
35
|
|
|
@@ -58,6 +58,8 @@ import buildValidator from './buildValidator.mjs'
|
|
|
58
58
|
import strTruncate from 'wsemi/src/strTruncate.mjs'
|
|
59
59
|
import getErrorResult from './getErrorResult.mjs'
|
|
60
60
|
import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
61
|
+
import { normalizeFinishReason, safeValidate, judgeTruncated } from './checkTruncation.mjs'
|
|
62
|
+
import describeNonJsonBody from './describeNonJsonBody.mjs'
|
|
61
63
|
|
|
62
64
|
|
|
63
65
|
// dispatchApiOpenaiResponses.mjs — 以fetch直呼OpenAI Responses API(/responses)
|
|
@@ -80,16 +82,25 @@ import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
|
80
82
|
// (chat/completions為prompt_tokens/completion_tokens/total_tokens)。
|
|
81
83
|
// 本套件usage一律原樣透傳不做正規化, 跨kind加總時呼叫端須自行對應欄位名。
|
|
82
84
|
//
|
|
83
|
-
// 【status不為completed
|
|
85
|
+
// 【status不為completed預設失敗, 不回半截內容】incomplete(如max_output_tokens
|
|
84
86
|
// 耗盡)之output常為空陣列或截斷內容, 當成功回傳會讓截斷結果流入下游而無人察覺;
|
|
85
87
|
// 故以INCOMPLETE_RESPONSE回報(errorType為incomplete), 呼叫端據此調高max_output_tokens
|
|
86
88
|
// 或換家。實測: max_output_tokens為16時status為incomplete、incomplete_details為
|
|
87
89
|
// {reason:'max_output_tokens'}、output為空陣列。
|
|
90
|
+
// 2026-09-24起與dispatchApiOpenaiCompat同一截斷規則(單一來源checkTruncation.mjs): 僅status為incomplete
|
|
91
|
+
// 視為截斷(結果帶truncated:true, finishReason將max_output_tokens正規化為length), 呼叫端明示
|
|
92
|
+
// acceptTruncated:true才放行length之截斷(交validate裁決); failed/缺status/處理中等屬非截斷之失敗,
|
|
93
|
+
// truncated:false, 遞補層照舊換金鑰(服務回錯)。可見輸出為空之截斷訊息附reasoning_tokens。
|
|
88
94
|
//
|
|
89
95
|
// 【不支援工具, 與dispatchApiOpenaiCompat同一決策】output含function_call型元素時
|
|
90
96
|
// 以TOOL_CALLS_UNSUPPORTED回報而不假裝成功; 理由(工具迴圈須自建harness、tool_call
|
|
91
97
|
// 有會話束縛無法外傳上層agent)詳見dispatchApiOpenaiCompat.mjs與adapters.mjs檔頭。
|
|
92
98
|
//
|
|
99
|
+
// 【預設帶Accept-Encoding: identity(2026-09-24起)】防伺服器壓縮卻漏標Content-Encoding而令fetch不解壓、
|
|
100
|
+
// JSON.parse失敗; 與dispatchApiOpenaiCompat同一決策與依據(見該檔檔頭), opt.headers可覆寫。
|
|
101
|
+
// HTTP 200但本體非JSON時另報INVALID_RESPONSE: body is not JSON(附原始位元組資訊), 與「缺output陣列」分開
|
|
102
|
+
// (同dispatchApiOpenaiCompat, 見describeNonJsonBody.mjs)。
|
|
103
|
+
//
|
|
93
104
|
// 【錯誤碼實測(Zen)】壞金鑰401(AuthError); 未知model亦回401(ModelError: Model X is not
|
|
94
105
|
// supported)而非404——故不可用狀態碼區分「金鑰錯」與「模型名錯」, 須讀stderr之訊息。
|
|
95
106
|
//
|
|
@@ -162,9 +173,10 @@ function extractOutputText(output) {
|
|
|
162
173
|
* @param {Object} body 輸入請求本體物件
|
|
163
174
|
* @param {Number} timeoutMs 輸入逾時毫秒
|
|
164
175
|
* @param {Function|null} validator 輸入驗證函式
|
|
176
|
+
* @param {Boolean} acceptTruncated 輸入是否明示接受截斷內容
|
|
165
177
|
* @returns {Promise} 回傳Promise,resolve回傳結果物件
|
|
166
178
|
*/
|
|
167
|
-
async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
179
|
+
async function callOnce(url, headers, body, timeoutMs, validator, acceptTruncated) {
|
|
168
180
|
|
|
169
181
|
let t0 = Date.now()
|
|
170
182
|
|
|
@@ -187,7 +199,9 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
187
199
|
controller.abort()
|
|
188
200
|
}, timeoutMs)
|
|
189
201
|
|
|
202
|
+
//本體先取原始位元組再以UTF-8解碼(等同res.text()), 本體非JSON時才有原始位元組可供診斷(見describeNonJsonBody.mjs)
|
|
190
203
|
let res = null
|
|
204
|
+
let raw = new Uint8Array(0)
|
|
191
205
|
let txt = ''
|
|
192
206
|
try {
|
|
193
207
|
res = await fetch(url, {
|
|
@@ -196,7 +210,8 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
196
210
|
body: JSON.stringify(body),
|
|
197
211
|
signal: controller.signal,
|
|
198
212
|
})
|
|
199
|
-
|
|
213
|
+
raw = new Uint8Array(await res.arrayBuffer())
|
|
214
|
+
txt = new TextDecoder('utf-8').decode(raw)
|
|
200
215
|
}
|
|
201
216
|
catch (err) {
|
|
202
217
|
clearTimeout(timer)
|
|
@@ -244,18 +259,36 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
244
259
|
catch {
|
|
245
260
|
parsed = false
|
|
246
261
|
}
|
|
247
|
-
|
|
262
|
+
//本體非JSON, 與「JSON缺output」分開回報並附原始位元組資訊; 遞補層據前綴整組跳過(見describeNonJsonBody.mjs)
|
|
263
|
+
if (!parsed) {
|
|
248
264
|
return mkResult({
|
|
249
265
|
stderr: strTruncate(txt, 500, optTruncate),
|
|
250
266
|
code: res.status,
|
|
251
|
-
error:
|
|
267
|
+
error: describeNonJsonBody(raw, res.headers),
|
|
252
268
|
errorType: 'invalid-response',
|
|
253
269
|
usage,
|
|
270
|
+
finishReason: '',
|
|
271
|
+
truncated: false,
|
|
254
272
|
})
|
|
255
273
|
}
|
|
256
274
|
|
|
275
|
+
//fin, 自此起之每個結果皆外顯正規化終止原因與是否截斷(與chat/completions對齊, 規則見checkTruncation.mjs):
|
|
276
|
+
//completed→stop; incomplete→依reason(max_output_tokens→length、content_filter原值、缺→incomplete); 其餘status原值; 缺status→''
|
|
277
|
+
let statusN = normalizeFinishReason(status)
|
|
278
|
+
let reasonN = normalizeFinishReason(incompleteReason)
|
|
279
|
+
let finishReason = statusN
|
|
280
|
+
if (statusN === 'completed') {
|
|
281
|
+
finishReason = 'stop'
|
|
282
|
+
}
|
|
283
|
+
else if (statusN === 'incomplete') {
|
|
284
|
+
finishReason = (reasonN === 'max_output_tokens') ? 'length' : (reasonN || 'incomplete')
|
|
285
|
+
}
|
|
286
|
+
let fin = { finishReason, truncated: statusN === 'incomplete' }
|
|
287
|
+
let detail = incompleteReason || failMsg || status || 'unknown'
|
|
288
|
+
let isArr = isarr(output)
|
|
289
|
+
|
|
257
290
|
//function_call, 本轉接器不支援工具迴圈(見檔頭), 明確回報而不假裝成功
|
|
258
|
-
let hasToolCall = output.some((o) => get(o, 'type', '') === 'function_call')
|
|
291
|
+
let hasToolCall = isArr && output.some((o) => get(o, 'type', '') === 'function_call')
|
|
259
292
|
if (hasToolCall) {
|
|
260
293
|
return mkResult({
|
|
261
294
|
stderr: strTruncate(txt, 1000, optTruncate),
|
|
@@ -263,12 +296,48 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
263
296
|
error: 'TOOL_CALLS_UNSUPPORTED: use a cli kind (opencode/claude/codex/antigravity) when tools are needed',
|
|
264
297
|
errorType: 'tool-unsupported',
|
|
265
298
|
usage,
|
|
299
|
+
...fin,
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
//截斷(status為incomplete), 於output檢查與validate之前裁定: 預設失敗, 明示acceptTruncated才交validate放行(見檔頭)
|
|
304
|
+
if (fin.truncated) {
|
|
305
|
+
let partial = isArr ? extractOutputText(output) : ''
|
|
306
|
+
let d = judgeTruncated({
|
|
307
|
+
finishReason,
|
|
308
|
+
content: partial,
|
|
309
|
+
acceptTruncated,
|
|
310
|
+
validator,
|
|
311
|
+
reasoningTokens: get(usage, 'output_tokens_details.reasoning_tokens', null),
|
|
312
|
+
label: `status=${status} (${detail})`,
|
|
266
313
|
})
|
|
314
|
+
if (!d.accept) {
|
|
315
|
+
return mkResult({
|
|
316
|
+
stdout: strTruncate(partial, 500, optTruncate),
|
|
317
|
+
stderr: strTruncate((d.threw ? `validate threw: ${d.threw}\n` : '') + txt, 500, optTruncate),
|
|
318
|
+
code: res.status,
|
|
319
|
+
error: d.error,
|
|
320
|
+
errorType: 'incomplete',
|
|
321
|
+
usage,
|
|
322
|
+
...fin,
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
return mkResult({ ok: true, stdout: partial, code: res.status, usage, ...fin })
|
|
267
326
|
}
|
|
268
327
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
328
|
+
if (!isArr) {
|
|
329
|
+
return mkResult({
|
|
330
|
+
stderr: strTruncate(txt, 500, optTruncate),
|
|
331
|
+
code: res.status,
|
|
332
|
+
error: 'INVALID_RESPONSE: missing output array',
|
|
333
|
+
errorType: 'invalid-response',
|
|
334
|
+
usage,
|
|
335
|
+
...fin,
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
//其餘非completed(failed/缺status/處理中等)屬非截斷之失敗, 不回半截內容; truncated:false故遞補層照舊換金鑰
|
|
340
|
+
if (statusN !== 'completed') {
|
|
272
341
|
return mkResult({
|
|
273
342
|
stdout: strTruncate(extractOutputText(output), 500, optTruncate),
|
|
274
343
|
stderr: strTruncate(txt, 500, optTruncate),
|
|
@@ -276,6 +345,7 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
276
345
|
error: `INCOMPLETE_RESPONSE: status=${status || 'missing'} (${detail})`,
|
|
277
346
|
errorType: 'incomplete',
|
|
278
347
|
usage,
|
|
348
|
+
...fin,
|
|
279
349
|
})
|
|
280
350
|
}
|
|
281
351
|
|
|
@@ -288,18 +358,24 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
288
358
|
error: 'INVALID_RESPONSE: no output_text in output messages',
|
|
289
359
|
errorType: 'invalid-response',
|
|
290
360
|
usage,
|
|
361
|
+
...fin,
|
|
291
362
|
})
|
|
292
363
|
}
|
|
293
364
|
|
|
294
|
-
//validator, error與execCli一致令dispatchAiFallback
|
|
295
|
-
if (validator
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
365
|
+
//validator, error與execCli一致令dispatchAiFallback可統一分流; 拋錯視同拒絕(不reject), 訊息置stderr
|
|
366
|
+
if (validator) {
|
|
367
|
+
let v = safeValidate(validator, content)
|
|
368
|
+
if (!v.pass) {
|
|
369
|
+
return mkResult({
|
|
370
|
+
stdout: strTruncate(content, 500, optTruncate),
|
|
371
|
+
stderr: v.threw ? `validate threw: ${v.threw}` : '',
|
|
372
|
+
code: res.status,
|
|
373
|
+
error: 'OUTPUT_VALIDATION_FAILED',
|
|
374
|
+
errorType: 'validation',
|
|
375
|
+
usage,
|
|
376
|
+
...fin,
|
|
377
|
+
})
|
|
378
|
+
}
|
|
303
379
|
}
|
|
304
380
|
|
|
305
381
|
return mkResult({
|
|
@@ -307,6 +383,7 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
307
383
|
stdout: content,
|
|
308
384
|
code: res.status,
|
|
309
385
|
usage,
|
|
386
|
+
...fin,
|
|
310
387
|
})
|
|
311
388
|
}
|
|
312
389
|
|
|
@@ -333,12 +410,13 @@ async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
|
333
410
|
* @param {String} [opt.key=''] 輸入API key字串,以Bearer置於Authorization標頭,預設''代表不帶認證標頭
|
|
334
411
|
* @param {String} [opt.system=''] 輸入system提示詞字串,將置於instructions欄位(Responses API之system管道),預設''代表不帶
|
|
335
412
|
* @param {Object} [opt.body={}] 輸入額外請求本體物件(如temperature、max_output_tokens、reasoning),將併入預設body(同名鍵以此為準),預設{}。注意輸出上限欄位名為max_output_tokens而非max_tokens;本轉接器不支援工具,帶入tools而模型回function_call時一律以TOOL_CALLS_UNSUPPORTED回報失敗
|
|
336
|
-
* @param {Object} [opt.headers={}]
|
|
413
|
+
* @param {Object} [opt.headers={}] 輸入額外請求標頭物件,同名鍵覆寫預設標頭;預設標頭含'Accept-Encoding: identity'(防伺服器壓縮卻漏標Content-Encoding,見檔頭),預設{}
|
|
337
414
|
* @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將中止請求(含回應串流讀取),全套件統一預設300000
|
|
338
|
-
* @param {String|Function} [opt.validate=undefined] 輸入回覆內容驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100'
|
|
339
|
-
* @param {
|
|
415
|
+
* @param {String|Function} [opt.validate=undefined] 輸入回覆內容驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,自訂函數拋錯視同驗證失敗,預設undefined代表不驗證
|
|
416
|
+
* @param {Boolean} [opt.acceptTruncated=false] 輸入是否接受被截斷(status為incomplete且reason為max_output_tokens)之內容布林值,true代表交validate裁決(無validate則直接接受)且結果標truncated:true;content_filter與可見輸出為空者一律失敗,預設false代表截斷一律失敗
|
|
417
|
+
* @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,4xx(429除外)與截斷不重試,預設0
|
|
340
418
|
* @param {Number} [opt.retryDelayMs=5000] 輸入重試間隔毫秒正整數,實際間隔為retryDelayMs乘以重試次數且上限15000ms,預設5000
|
|
341
|
-
* @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
|
|
419
|
+
* @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
|
|
342
420
|
* @example
|
|
343
421
|
* //need network, no cli required
|
|
344
422
|
*
|
|
@@ -421,6 +499,9 @@ async function dispatchApiOpenaiResponses(prompt, opt = {}) {
|
|
|
421
499
|
//validator
|
|
422
500
|
let validator = buildValidator(get(opt, 'validate', null))
|
|
423
501
|
|
|
502
|
+
//acceptTruncated, 僅明示true才放行截斷內容(見檔頭)
|
|
503
|
+
let acceptTruncated = get(opt, 'acceptTruncated', null) === true
|
|
504
|
+
|
|
424
505
|
//url, baseURL尾端斜線正規化後接上端點
|
|
425
506
|
let url = baseURL.replace(/\/+$/, '') + '/responses'
|
|
426
507
|
|
|
@@ -431,8 +512,8 @@ async function dispatchApiOpenaiResponses(prompt, opt = {}) {
|
|
|
431
512
|
}
|
|
432
513
|
body = { ...body, ...bodyExtra }
|
|
433
514
|
|
|
434
|
-
//headers
|
|
435
|
-
let headers = { 'Content-Type': 'application/json', ...headersExtra }
|
|
515
|
+
//headers, Accept-Encoding預設identity(防伺服器壓縮卻漏標Content-Encoding, 見檔頭), headersExtra同名鍵可覆寫
|
|
516
|
+
let headers = { 'Content-Type': 'application/json', 'Accept-Encoding': 'identity', ...headersExtra }
|
|
436
517
|
if (isestr(key)) {
|
|
437
518
|
headers['Authorization'] = `Bearer ${key}`
|
|
438
519
|
}
|
|
@@ -447,7 +528,7 @@ async function dispatchApiOpenaiResponses(prompt, opt = {}) {
|
|
|
447
528
|
await delay(Math.min(retryDelayMs * attempt, MAX_RETRY_DELAY_MS))
|
|
448
529
|
}
|
|
449
530
|
|
|
450
|
-
lastResult = await callOnce(url, headers, body, timeoutMs, validator)
|
|
531
|
+
lastResult = await callOnce(url, headers, body, timeoutMs, validator, acceptTruncated)
|
|
451
532
|
totalAttempts = attempt + 1
|
|
452
533
|
|
|
453
534
|
if (lastResult.ok) {
|
|
@@ -455,6 +536,11 @@ async function dispatchApiOpenaiResponses(prompt, opt = {}) {
|
|
|
455
536
|
return lastResult
|
|
456
537
|
}
|
|
457
538
|
|
|
539
|
+
//不可重試: 截斷為決定性(同一請求必然再截斷, 重試只會再燒一次輸出上限)
|
|
540
|
+
if (lastResult.truncated === true) {
|
|
541
|
+
break
|
|
542
|
+
}
|
|
543
|
+
|
|
458
544
|
//不可重試: 4xx(429除外)為客戶端錯誤, 重試無意義
|
|
459
545
|
let c = lastResult.code
|
|
460
546
|
if (isnum(c) && c >= 400 && c < 500 && c !== 429) {
|
|
@@ -485,7 +571,7 @@ export { extractOutputText }
|
|
|
485
571
|
<br class="clear">
|
|
486
572
|
|
|
487
573
|
<footer>
|
|
488
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Sep 24 2026
|
|
574
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Sep 24 2026 15:32:02 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
489
575
|
</footer>
|
|
490
576
|
|
|
491
577
|
<script>prettyPrint();</script>
|