w-dispatch-ai 1.0.24 → 1.0.26
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 +57 -5
- package/dist/w-dispatch-ai.umd.js +2 -2
- package/dist/w-dispatch-ai.umd.js.map +1 -1
- package/docs/WDispatchAi.mjs.html +6 -4
- package/docs/adapters.mjs.html +10 -3
- package/docs/budgetFor.mjs.html +2 -2
- package/docs/buildValidator.mjs.html +2 -2
- package/docs/castPintOr.mjs.html +2 -2
- package/docs/dfTimeoutMs.mjs.html +2 -2
- package/docs/dispatchAi.mjs.html +2 -2
- package/docs/dispatchAiFallback.mjs.html +2 -2
- package/docs/dispatchAiWkf.mjs.html +2 -2
- package/docs/dispatchAntigravity.mjs.html +2 -2
- package/docs/dispatchApiOpenaiCompat.mjs.html +2 -2
- package/docs/dispatchApiOpenaiResponses.mjs.html +2 -2
- package/docs/dispatchApiTypesafeSystemone.mjs.html +447 -0
- 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 +4 -3
- package/docs/global.html +1223 -295
- 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 +62 -62
- package/docs/resolveProviders.mjs.html +183 -183
- package/docs/wkf_callAiWithFallback.mjs.html +2 -2
- 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 +2 -2
- package/docs/wkf_runFanoutPipeline.mjs.html +2 -2
- package/docs/wkf_runRolePipeline.mjs.html +2 -2
- package/docs/wkf_salvageTruncatedArray.mjs.html +2 -2
- package/g.mjs +2 -2
- package/package.json +1 -6
- package/src/WDispatchAi.mjs +4 -2
- package/src/adapters.mjs +8 -1
- package/src/dispatchApiTypesafeSystemone.mjs +375 -0
- package/src/getErrorType.mjs +2 -1
- package/src/providers.mjs +336 -303
- package/src/readEnvFile.mjs +60 -60
- package/src/resolveProviders.mjs +181 -181
- package/test/tools/fakeServerForApiTest.mjs +75 -5
- package/test/unit-WDispatchAi.test.mjs +10 -6
- package/test/unit-adapters.test.mjs +5 -3
- package/test/unit-dispatchAi.test.mjs +1 -1
- package/test/unit-dispatchApiTypesafeSystemone.test.mjs +159 -0
- package/test/unit-providers.test.mjs +7 -0
- package/test/unit-resolveProviders.test.mjs +2 -2
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import get from 'lodash-es/get.js'
|
|
2
|
+
import isobj from 'wsemi/src/isobj.mjs'
|
|
3
|
+
import iseobj from 'wsemi/src/iseobj.mjs'
|
|
4
|
+
import isnum from 'wsemi/src/isnum.mjs'
|
|
5
|
+
import cint from 'wsemi/src/cint.mjs'
|
|
6
|
+
import isestr from 'wsemi/src/isestr.mjs'
|
|
7
|
+
import isp0int from 'wsemi/src/isp0int.mjs'
|
|
8
|
+
import delay from 'wsemi/src/delay.mjs'
|
|
9
|
+
import strTruncate from 'wsemi/src/strTruncate.mjs'
|
|
10
|
+
import castPintOr from './castPintOr.mjs'
|
|
11
|
+
import buildValidator from './buildValidator.mjs'
|
|
12
|
+
import getErrorResult from './getErrorResult.mjs'
|
|
13
|
+
import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
// dispatchApiTypesafeSystemone.mjs — 以fetch直呼TypeSafe AI之System One API(POST /v1/systemone)
|
|
17
|
+
//
|
|
18
|
+
// 【這不是文字生成模型(2026-09-17實測後新增)】TypeSafe之jev為「System One」決策模型:
|
|
19
|
+
// 不產生文字, 而是對一段state(被評估的內容)回答呼叫端定義的型別化問題(questions),
|
|
20
|
+
// 每題回傳受限於呼叫端選項之答案與機率。權威文件: https://docs.typesafe.ai/api
|
|
21
|
+
// (索引 https://docs.typesafe.ai/llms.txt)。官方僅提供Python/JavaScript SDK、Playground與
|
|
22
|
+
// agent skill, 無CLI(npm之@typesafe-ai/sdk無bin、PyPI之typesafe-sdk無console script), 故只有API版。
|
|
23
|
+
//
|
|
24
|
+
// 【請求】POST <baseURL>/systemone, Authorization: Bearer <key>, body為{ model, state, questions }:
|
|
25
|
+
// state ← 本轉接器之prompt(字串)。結構化內容請以JSON.stringify(物件)傳入——2026-09-17實測
|
|
26
|
+
// 同一組引用欄位路徑之問題, 物件state與JSON字串state之答案一致(差異≤0.01)。
|
|
27
|
+
// questions ← opt.questions(必填, 非空物件), 鍵為呼叫端自訂之題目id, 值為:
|
|
28
|
+
// { type:'noul', instructions, criteria?:{true,false} } 是非題, 回yes之機率
|
|
29
|
+
// { type:'choice', instructions, criteria:{選項:描述|null} } 單選題
|
|
30
|
+
// { type:'score', instructions, criteria:[層級描述...] } 有序量表(至少2級)
|
|
31
|
+
// 題型與欄位由伺服器驗證(不合規回422), 本轉接器只擋「未給questions」, 不重複伺服器規則。
|
|
32
|
+
// model ← opt.model, 預設'jev-latest'(官方SDK之預設; 回應之model為實際版本如'jev-1.13.0')。
|
|
33
|
+
//
|
|
34
|
+
// 【回應(實測)】{ model:'jev-1.13.0', answers:{ <id>:{type,choice|score|noul,probabilities?,confidence?,legend?} },
|
|
35
|
+
// usage:{ input_tokens, output_tokens } }; 回應標頭帶x-typesafe-request-id。
|
|
36
|
+
// 本轉接器之stdout為JSON.stringify(answers)(令dispatchAiFallback與工作流層之parse/check無須修改即可用),
|
|
37
|
+
// 另於結果物件追加answers(已解析物件)與modelResolved(實際模型版本)。
|
|
38
|
+
// 請求的題目id在answers中缺任一個即視為INVALID_RESPONSE, 不讓下游取到undefined。
|
|
39
|
+
//
|
|
40
|
+
// 【錯誤(實測)】壞金鑰401 {detail:{error_type:'authentication_error'}}; 未知model 400
|
|
41
|
+
// {detail:{error_type:'api_usage_error', message:'Unknown model: X'}}; 題型不合規或questions為空422
|
|
42
|
+
// {detail:[{type,loc,msg,...}]}。皆以HTTP <code>回報、原始本體置stderr; 4xx(429除外)不重試。
|
|
43
|
+
//
|
|
44
|
+
// 【混用注意】
|
|
45
|
+
// 1. 答案形狀與文字模型完全不同, 不可與文字生成條目混在同一條dispatchAiFallback鏈中遞補;
|
|
46
|
+
// 同一鏈只放本kind之條目(可多把金鑰輪替)。預設providers.mjs收有typesafe:jev-latest,
|
|
47
|
+
// 請以pick單獨取出; 全取做文字遞補時該條因無questions以params錯誤0ms失敗後換下一家。
|
|
48
|
+
// 2. questions於dispatchAiFallback可放呼叫層opt(非fallback自用鍵會透傳給各條目), 不必寫進條目。
|
|
49
|
+
// 3. 工作流callAi預設在prompt前掛防寫檔前綴(NO_SIDE_EFFECT), 對本kind等於把前綴混進被評估的state,
|
|
50
|
+
// 經工作流呼叫時務必傳promptPrefix:''。
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
//預設值
|
|
54
|
+
let DEFAULT_BASE_URL = 'https://api.typesafe.ai/v1'
|
|
55
|
+
let DEFAULT_MODEL = 'jev-latest'
|
|
56
|
+
let DEFAULT_TIMEOUT_MS = dfTimeoutMs //全套件統一預設300000
|
|
57
|
+
let DEFAULT_RETRY_DELAY_MS = 5000
|
|
58
|
+
let MAX_RETRY_DELAY_MS = 15000
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
//optTruncate, 裁切失敗結果之內容時於刪節號後標註原始總長度(同execCli)
|
|
62
|
+
let optTruncate = {
|
|
63
|
+
funWithMsg: (str) => `(truncated, total ${str.length} chars)`,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 單次HTTP呼叫(內部使用, 不含重試邏輯)
|
|
69
|
+
*
|
|
70
|
+
* @param {String} url 輸入完整端點網址字串
|
|
71
|
+
* @param {Object} headers 輸入請求標頭物件
|
|
72
|
+
* @param {Object} body 輸入請求本體物件
|
|
73
|
+
* @param {Array} ids 輸入請求之題目id陣列,用於檢核回應是否每題皆有答案
|
|
74
|
+
* @param {Number} timeoutMs 輸入逾時毫秒
|
|
75
|
+
* @param {Function|null} validator 輸入驗證函式
|
|
76
|
+
* @returns {Promise} 回傳Promise,resolve回傳結果物件
|
|
77
|
+
*/
|
|
78
|
+
async function callOnce(url, headers, body, ids, timeoutMs, validator) {
|
|
79
|
+
|
|
80
|
+
let t0 = Date.now()
|
|
81
|
+
|
|
82
|
+
//mkResult, 結果形狀之單一來源(欄位對齊其他api類轉接器, 追加answers與modelResolved)
|
|
83
|
+
let mkResult = (patch) => ({
|
|
84
|
+
ok: false,
|
|
85
|
+
stdout: '',
|
|
86
|
+
stderr: '',
|
|
87
|
+
code: null,
|
|
88
|
+
error: '',
|
|
89
|
+
durationMs: Date.now() - t0,
|
|
90
|
+
usage: null,
|
|
91
|
+
answers: null,
|
|
92
|
+
modelResolved: '',
|
|
93
|
+
...patch,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
//AbortController, 逾時中止(含回應本體之讀取)
|
|
97
|
+
let controller = new AbortController()
|
|
98
|
+
let timer = setTimeout(() => {
|
|
99
|
+
controller.abort()
|
|
100
|
+
}, timeoutMs)
|
|
101
|
+
|
|
102
|
+
let res = null
|
|
103
|
+
let txt = ''
|
|
104
|
+
try {
|
|
105
|
+
res = await fetch(url, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers,
|
|
108
|
+
body: JSON.stringify(body),
|
|
109
|
+
signal: controller.signal,
|
|
110
|
+
})
|
|
111
|
+
txt = await res.text()
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
clearTimeout(timer)
|
|
115
|
+
|
|
116
|
+
//逾時, error以TIMEOUT開頭令dispatchAiFallback視為與金鑰無關而跳組
|
|
117
|
+
if (err.name === 'AbortError') {
|
|
118
|
+
return mkResult({ error: `TIMEOUT after ${timeoutMs / 1000}s`, errorType: 'timeout' })
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//網路層錯誤(DNS/連線拒絕等)
|
|
122
|
+
let cause = get(err, 'cause.code', '') || err.message
|
|
123
|
+
return mkResult({ error: `FETCH_ERROR: ${cause}`, errorType: 'fetch' })
|
|
124
|
+
}
|
|
125
|
+
clearTimeout(timer)
|
|
126
|
+
|
|
127
|
+
//HTTP非2xx, 原始回應本體放stderr(401/400之detail.message、422之detail陣列皆在其中)
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
return mkResult({
|
|
130
|
+
stderr: strTruncate(txt, 1000, optTruncate),
|
|
131
|
+
code: res.status,
|
|
132
|
+
error: `HTTP ${res.status}`,
|
|
133
|
+
errorType: 'http',
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
//解析answers/model/usage
|
|
138
|
+
let answers = null
|
|
139
|
+
let modelResolved = ''
|
|
140
|
+
let usage = null
|
|
141
|
+
try {
|
|
142
|
+
let j = JSON.parse(txt)
|
|
143
|
+
answers = get(j, 'answers', null)
|
|
144
|
+
modelResolved = get(j, 'model', '')
|
|
145
|
+
if (!isestr(modelResolved)) {
|
|
146
|
+
modelResolved = ''
|
|
147
|
+
}
|
|
148
|
+
usage = get(j, 'usage', null)
|
|
149
|
+
if (!isobj(usage)) {
|
|
150
|
+
usage = null
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
answers = null
|
|
155
|
+
}
|
|
156
|
+
if (!isobj(answers)) {
|
|
157
|
+
return mkResult({
|
|
158
|
+
stderr: strTruncate(txt, 500, optTruncate),
|
|
159
|
+
code: res.status,
|
|
160
|
+
error: 'INVALID_RESPONSE: missing answers object',
|
|
161
|
+
errorType: 'invalid-response',
|
|
162
|
+
usage,
|
|
163
|
+
modelResolved,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
//每個請求之題目皆須有答案, 缺漏即不合規(避免下游取到undefined)
|
|
168
|
+
let missing = ids.filter((id) => !isobj(answers[id]))
|
|
169
|
+
if (missing.length > 0) {
|
|
170
|
+
return mkResult({
|
|
171
|
+
stderr: strTruncate(txt, 500, optTruncate),
|
|
172
|
+
code: res.status,
|
|
173
|
+
error: `INVALID_RESPONSE: missing answers for [${missing.join(', ')}]`,
|
|
174
|
+
errorType: 'invalid-response',
|
|
175
|
+
usage,
|
|
176
|
+
modelResolved,
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
//content, answers序列化為stdout(令遞補層與工作流層之parse/check通用)
|
|
181
|
+
let content = JSON.stringify(answers)
|
|
182
|
+
|
|
183
|
+
//validator, error與execCli一致令dispatchAiFallback可統一分流
|
|
184
|
+
if (validator && !validator(content)) {
|
|
185
|
+
return mkResult({
|
|
186
|
+
stdout: strTruncate(content, 500, optTruncate),
|
|
187
|
+
code: res.status,
|
|
188
|
+
error: 'OUTPUT_VALIDATION_FAILED',
|
|
189
|
+
errorType: 'validation',
|
|
190
|
+
usage,
|
|
191
|
+
modelResolved,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return mkResult({
|
|
196
|
+
ok: true,
|
|
197
|
+
stdout: content,
|
|
198
|
+
code: res.status,
|
|
199
|
+
usage,
|
|
200
|
+
answers,
|
|
201
|
+
modelResolved,
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
//本轉接器不使用execCli, 全部設定鍵自理, 未知鍵一律忽略
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 以fetch直呼TypeSafe AI之System One API(POST /v1/systemone),對prompt(即state)回答型別化問題
|
|
211
|
+
*
|
|
212
|
+
* 特點:
|
|
213
|
+
* 非文字生成——jev等System One模型對state回答呼叫端定義之questions(noul是非題、choice單選題、score量表),
|
|
214
|
+
* 每題回傳受限於選項之答案與機率;官方無CLI,僅此API路徑;
|
|
215
|
+
* prompt即state,結構化內容以JSON.stringify(物件)傳入(實測答案與物件state一致);
|
|
216
|
+
* stdout為answers之JSON字串,另於結果追加answers(已解析物件)與modelResolved(實際模型版本);
|
|
217
|
+
* 請求之題目id於回應缺任一即以INVALID_RESPONSE回報;
|
|
218
|
+
* 不可與文字生成條目混在同一條遞補鏈,經工作流callAi呼叫時須傳promptPrefix:''(理由見檔頭);
|
|
219
|
+
* 本函數不會reject,一律以結果物件之ok與error欄位回報成敗
|
|
220
|
+
*
|
|
221
|
+
* @param {String} prompt 輸入被評估之內容字串,作為state置於HTTP body,結構化內容請先JSON.stringify
|
|
222
|
+
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
223
|
+
* @param {Object} opt.questions 輸入題目物件(必填且非空),鍵為自訂題目id,值為{type:'noul'|'choice'|'score', instructions, criteria},格式見檔頭與https://docs.typesafe.ai/api
|
|
224
|
+
* @param {String} [opt.baseURL='https://api.typesafe.ai/v1'] 輸入API基底網址字串,將於尾端接上/systemone,預設官方端點
|
|
225
|
+
* @param {String} [opt.model='jev-latest'] 輸入模型名稱字串,預設'jev-latest'(另有'jev-preview')
|
|
226
|
+
* @param {String} [opt.key=''] 輸入API key字串,以Bearer置於Authorization標頭,預設''代表不帶認證標頭
|
|
227
|
+
* @param {Object} [opt.body={}] 輸入額外請求本體物件,將併入預設body(同名鍵以此為準),預設{}
|
|
228
|
+
* @param {Object} [opt.headers={}] 輸入額外請求標頭物件,預設{}
|
|
229
|
+
* @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將中止請求,全套件統一預設300000
|
|
230
|
+
* @param {String|Function} [opt.validate=undefined] 輸入stdout(answers之JSON字串)驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,預設undefined代表不驗證
|
|
231
|
+
* @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,4xx(429除外)不重試,預設0
|
|
232
|
+
* @param {Number} [opt.retryDelayMs=5000] 輸入重試間隔毫秒正整數,實際間隔為retryDelayMs乘以重試次數且上限15000ms,預設5000
|
|
233
|
+
* @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
|
|
234
|
+
* @example
|
|
235
|
+
* //need network and a TypeSafe api key, no cli required
|
|
236
|
+
*
|
|
237
|
+
* import dispatchApiTypesafeSystemone from './src/dispatchApiTypesafeSystemone.mjs'
|
|
238
|
+
*
|
|
239
|
+
* let test = async () => {
|
|
240
|
+
*
|
|
241
|
+
* let r = await dispatchApiTypesafeSystemone('房間浴室水龍頭一直滴水,吵到睡不著', {
|
|
242
|
+
* key: 'apikey_xxxxxx',
|
|
243
|
+
* questions: {
|
|
244
|
+
* category: {
|
|
245
|
+
* type: 'choice',
|
|
246
|
+
* instructions: '這則客房訊息屬於哪一類?',
|
|
247
|
+
* criteria: {
|
|
248
|
+
* '設備故障報修': '客人回報房間硬體設備損壞、水電問題或故障',
|
|
249
|
+
* '索取備品': '客人需要毛巾、牙刷、礦泉水等客房備品',
|
|
250
|
+
* '其他': null,
|
|
251
|
+
* },
|
|
252
|
+
* },
|
|
253
|
+
* urgent: { type: 'noul', instructions: '客人是否表達急迫性?' },
|
|
254
|
+
* },
|
|
255
|
+
* })
|
|
256
|
+
* console.log(r.ok, r.answers.category.choice, r.answers.urgent.noul)
|
|
257
|
+
* // => true 設備故障報修 0.86 (2026-09-17實測; noul為機率, 每次可能差0.01)
|
|
258
|
+
*
|
|
259
|
+
* let re = await dispatchApiTypesafeSystemone('abc', { key: 'apikey_bad', questions: { a: { type: 'noul', instructions: 'x?' } } })
|
|
260
|
+
* console.log(re.ok, re.code, re.errorType)
|
|
261
|
+
* // => false 401 http
|
|
262
|
+
*
|
|
263
|
+
* }
|
|
264
|
+
* await test()
|
|
265
|
+
* .catch((err) => {
|
|
266
|
+
* console.log(err)
|
|
267
|
+
* })
|
|
268
|
+
*
|
|
269
|
+
*/
|
|
270
|
+
async function dispatchApiTypesafeSystemone(prompt, opt = {}) {
|
|
271
|
+
|
|
272
|
+
//check prompt, 不reject故以錯誤結果物件回報
|
|
273
|
+
if (!isestr(prompt)) {
|
|
274
|
+
return getErrorResult('prompt must be a non-empty string')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
//questions必填且非空, 題型細節交伺服器驗證(回422)
|
|
278
|
+
let questions = get(opt, 'questions', null)
|
|
279
|
+
if (!iseobj(questions)) {
|
|
280
|
+
return getErrorResult('questions must be a non-empty object')
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
//baseURL, 無效回退官方端點
|
|
284
|
+
let baseURL = get(opt, 'baseURL', null)
|
|
285
|
+
if (!isestr(baseURL)) {
|
|
286
|
+
baseURL = DEFAULT_BASE_URL
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
//model, 無效回退官方預設
|
|
290
|
+
let model = get(opt, 'model', null)
|
|
291
|
+
if (!isestr(model)) {
|
|
292
|
+
model = DEFAULT_MODEL
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
//key, 無效代表不帶認證標頭
|
|
296
|
+
let key = get(opt, 'key', null)
|
|
297
|
+
|
|
298
|
+
//bodyExtra
|
|
299
|
+
let bodyExtra = get(opt, 'body', null)
|
|
300
|
+
if (!isobj(bodyExtra)) {
|
|
301
|
+
bodyExtra = {}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
//headersExtra
|
|
305
|
+
let headersExtra = get(opt, 'headers', null)
|
|
306
|
+
if (!isobj(headersExtra)) {
|
|
307
|
+
headersExtra = {}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
//timeoutMs
|
|
311
|
+
let timeoutMs = castPintOr(get(opt, 'timeoutMs', null), DEFAULT_TIMEOUT_MS)
|
|
312
|
+
|
|
313
|
+
//maxRetries
|
|
314
|
+
let maxRetries = get(opt, 'maxRetries', null)
|
|
315
|
+
if (!isp0int(maxRetries)) {
|
|
316
|
+
maxRetries = 0
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
maxRetries = cint(maxRetries)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
//retryDelayMs
|
|
323
|
+
let retryDelayMs = castPintOr(get(opt, 'retryDelayMs', null), DEFAULT_RETRY_DELAY_MS)
|
|
324
|
+
|
|
325
|
+
//validator
|
|
326
|
+
let validator = buildValidator(get(opt, 'validate', null))
|
|
327
|
+
|
|
328
|
+
//url, baseURL尾端斜線正規化後接上端點
|
|
329
|
+
let url = baseURL.replace(/\/+$/, '') + '/systemone'
|
|
330
|
+
|
|
331
|
+
//body, prompt即state; 額外鍵以bodyExtra為準
|
|
332
|
+
let body = { model, state: prompt, questions, ...bodyExtra }
|
|
333
|
+
|
|
334
|
+
//ids, 用於檢核回應是否每題皆有答案(以實際送出之questions為準)
|
|
335
|
+
let ids = Object.keys(isobj(body.questions) ? body.questions : questions)
|
|
336
|
+
|
|
337
|
+
//headers
|
|
338
|
+
let headers = { 'Content-Type': 'application/json', ...headersExtra }
|
|
339
|
+
if (isestr(key)) {
|
|
340
|
+
headers['Authorization'] = `Bearer ${key}`
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let lastResult = null
|
|
344
|
+
let totalAttempts = 0
|
|
345
|
+
|
|
346
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
347
|
+
|
|
348
|
+
//delay, 重試間隔隨次數遞增, 上限15000ms(同execCli)
|
|
349
|
+
if (attempt > 0) {
|
|
350
|
+
await delay(Math.min(retryDelayMs * attempt, MAX_RETRY_DELAY_MS))
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
lastResult = await callOnce(url, headers, body, ids, timeoutMs, validator)
|
|
354
|
+
totalAttempts = attempt + 1
|
|
355
|
+
|
|
356
|
+
if (lastResult.ok) {
|
|
357
|
+
lastResult.attempts = totalAttempts
|
|
358
|
+
return lastResult
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
//不可重試: 4xx(429除外)為客戶端錯誤, 重試無意義
|
|
362
|
+
let c = lastResult.code
|
|
363
|
+
if (isnum(c) && c >= 400 && c < 500 && c !== 429) {
|
|
364
|
+
break
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
lastResult.attempts = totalAttempts
|
|
370
|
+
|
|
371
|
+
return lastResult
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
export default dispatchApiTypesafeSystemone
|
package/src/getErrorType.mjs
CHANGED
|
@@ -22,7 +22,8 @@ import isestr from 'wsemi/src/isestr.mjs'
|
|
|
22
22
|
// 'http' HTTP非2xx(code為狀態碼, 僅api類)
|
|
23
23
|
// 'fetch' 網路層錯誤(DNS/連線拒絕, 僅api類)
|
|
24
24
|
// 'tool-unsupported' 模型回tool_calls而本轉接器不支援工具(僅api類)
|
|
25
|
-
// 'invalid-response' 回應結構不合規(缺choices[0].message.content
|
|
25
|
+
// 'invalid-response' 回應結構不合規(缺choices[0].message.content、缺output陣列,
|
|
26
|
+
// 或api-typesafe-systemone缺answers物件/缺所請求題目之答案, 僅api類)
|
|
26
27
|
// 'incomplete' Responses API之status非completed(如max_output_tokens耗盡而截斷,
|
|
27
28
|
// 僅api-openai-responses; 半截內容不當成功回傳, 見該轉接器檔頭)
|
|
28
29
|
// 'aborted' shouldStop中止(僅dispatchAiFallback)
|