w-dispatch-ai 1.0.21 → 1.0.22
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 +15 -3
- package/dist/w-dispatch-ai.umd.js +2 -2
- package/dist/w-dispatch-ai.umd.js.map +1 -1
- package/docs/WDispatchAi.mjs.html +5 -3
- package/docs/adapters.mjs.html +5 -3
- package/docs/budgetFor.mjs.html +2 -2
- package/docs/buildValidator.mjs.html +166 -0
- 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 +4 -68
- package/docs/dispatchApiOpenaiResponses.mjs.html +498 -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 +5 -3
- package/docs/global.html +2277 -1159
- package/docs/index.html +2 -2
- package/docs/readEnvFile.mjs.html +2 -2
- package/docs/resolveProviders.mjs.html +3 -3
- 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 -1
- package/src/WDispatchAi.mjs +3 -1
- package/src/adapters.mjs +3 -1
- package/src/buildValidator.mjs +94 -0
- package/src/dispatchApiOpenaiCompat.mjs +2 -66
- package/src/dispatchApiOpenaiResponses.mjs +426 -0
- package/src/getErrorType.mjs +3 -1
- package/src/providers.mjs +303 -213
- package/src/resolveProviders.mjs +1 -1
- package/test/tools/fakeServerForApiTest.mjs +108 -3
- 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-dispatchApiOpenaiResponses.test.mjs +198 -0
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import get from 'lodash-es/get.js'
|
|
2
2
|
import isobj from 'wsemi/src/isobj.mjs'
|
|
3
|
-
import isfun from 'wsemi/src/isfun.mjs'
|
|
4
3
|
import isnum from 'wsemi/src/isnum.mjs'
|
|
4
|
+
import cint from 'wsemi/src/cint.mjs'
|
|
5
5
|
import isestr from 'wsemi/src/isestr.mjs'
|
|
6
6
|
import isp0int from 'wsemi/src/isp0int.mjs'
|
|
7
|
-
import cint from 'wsemi/src/cint.mjs'
|
|
8
7
|
import delay from 'wsemi/src/delay.mjs'
|
|
9
8
|
import castPintOr from './castPintOr.mjs'
|
|
10
|
-
import
|
|
11
|
-
import strdelleft from 'wsemi/src/strdelleft.mjs'
|
|
9
|
+
import buildValidator from './buildValidator.mjs'
|
|
12
10
|
import strTruncate from 'wsemi/src/strTruncate.mjs'
|
|
13
11
|
import getErrorResult from './getErrorResult.mjs'
|
|
14
12
|
import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
@@ -66,68 +64,6 @@ let optTruncate = {
|
|
|
66
64
|
}
|
|
67
65
|
|
|
68
66
|
|
|
69
|
-
/**
|
|
70
|
-
* 建立驗證函式(規則語法同execCli之validate)
|
|
71
|
-
*
|
|
72
|
-
* @param {String|Function} rule 輸入驗證規則字串('nonempty'、'json'、'min:100', 逗號可串接)或自訂函式
|
|
73
|
-
* @returns {Function|null} 回傳驗證函式,無有效規則回傳null
|
|
74
|
-
*/
|
|
75
|
-
function buildValidator(rule) {
|
|
76
|
-
|
|
77
|
-
//自訂函式直接使用
|
|
78
|
-
if (isfun(rule)) {
|
|
79
|
-
return rule
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
//check
|
|
83
|
-
if (!isestr(rule)) {
|
|
84
|
-
return null
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
//checks
|
|
88
|
-
let checks = rule.split(',').map((r) => r.trim()).filter(Boolean)
|
|
89
|
-
if (checks.length === 0) {
|
|
90
|
-
return null
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return (stdout) => {
|
|
94
|
-
for (let check of checks) {
|
|
95
|
-
|
|
96
|
-
if (check === 'nonempty') {
|
|
97
|
-
if (!isestr(stdout) || stdout.trim() === '') {
|
|
98
|
-
return false
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
else if (check === 'json') {
|
|
103
|
-
try {
|
|
104
|
-
JSON.parse(stdout)
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
return false
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
else if (strleft(check, 4) === 'min:') {
|
|
112
|
-
|
|
113
|
-
//規則本身無效(如min:abc) → 視為驗證失敗, 不靜默跳過
|
|
114
|
-
let smin = strdelleft(check, 4)
|
|
115
|
-
if (!isnum(smin)) {
|
|
116
|
-
return false
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
let min = cint(smin)
|
|
120
|
-
if (!isestr(stdout) || stdout.length < min) {
|
|
121
|
-
return false
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
return true
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
67
|
/**
|
|
132
68
|
* 單次HTTP呼叫(內部使用, 不含重試邏輯)
|
|
133
69
|
*
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import get from 'lodash-es/get.js'
|
|
2
|
+
import isarr from 'wsemi/src/isarr.mjs'
|
|
3
|
+
import isobj from 'wsemi/src/isobj.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 castPintOr from './castPintOr.mjs'
|
|
10
|
+
import buildValidator from './buildValidator.mjs'
|
|
11
|
+
import strTruncate from 'wsemi/src/strTruncate.mjs'
|
|
12
|
+
import getErrorResult from './getErrorResult.mjs'
|
|
13
|
+
import dfTimeoutMs from './dfTimeoutMs.mjs'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
// dispatchApiOpenaiResponses.mjs — 以fetch直呼OpenAI Responses API(/responses)
|
|
17
|
+
//
|
|
18
|
+
// 【為何需要(2026-09-03實測後新增)】OpenCode Zen之端點「依模型家族而異」, 並非全部走
|
|
19
|
+
// chat/completions: 官方文件(https://opencode.ai/docs/zh-tw/zen/)之端點欄明載
|
|
20
|
+
// muse-spark系與GPT系走/responses(@ai-sdk/openai)、Claude系走/messages、Gemini系走
|
|
21
|
+
// /models/<id>, 僅deepseek/glm/kimi/minimax/nemotron/ling/mimo等走/chat/completions。
|
|
22
|
+
// 以dispatchApiOpenaiCompat打/responses型模型會得到HTTP 500而非404, 極易被誤判為
|
|
23
|
+
// 「模型故障」而反覆重試——實測muse-spark-1.2/1.3走/chat/completions連續10次500,
|
|
24
|
+
// 同金鑰同模型改打/responses立即200。本轉接器即為補上該端點型別。
|
|
25
|
+
//
|
|
26
|
+
// 【與chat/completions之協定差異(皆2026-09-03於Zen實測確認)】
|
|
27
|
+
// 請求: 單一輸入欄位input(字串或訊息陣列)而非messages; system提示走instructions;
|
|
28
|
+
// 輸出上限為max_output_tokens而非max_tokens。
|
|
29
|
+
// 回應: 無choices, 改為output陣列, 元素type可為'reasoning'(思考, 實測content為空)、
|
|
30
|
+
// 'message'(內容在content[].text, type為'output_text')、'function_call'(工具呼叫);
|
|
31
|
+
// 另有頂層status(completed/incomplete/failed)與incomplete_details。
|
|
32
|
+
// 用量: usage欄位名不同——input_tokens/output_tokens/total_tokens
|
|
33
|
+
// (chat/completions為prompt_tokens/completion_tokens/total_tokens)。
|
|
34
|
+
// 本套件usage一律原樣透傳不做正規化, 跨kind加總時呼叫端須自行對應欄位名。
|
|
35
|
+
//
|
|
36
|
+
// 【status不為completed一律視為失敗, 不回半截內容】incomplete(如max_output_tokens
|
|
37
|
+
// 耗盡)之output常為空陣列或截斷內容, 當成功回傳會讓截斷結果流入下游而無人察覺;
|
|
38
|
+
// 故以INCOMPLETE_RESPONSE回報(errorType為incomplete), 呼叫端據此調高max_output_tokens
|
|
39
|
+
// 或換家。實測: max_output_tokens為16時status為incomplete、incomplete_details為
|
|
40
|
+
// {reason:'max_output_tokens'}、output為空陣列。
|
|
41
|
+
//
|
|
42
|
+
// 【不支援工具, 與dispatchApiOpenaiCompat同一決策】output含function_call型元素時
|
|
43
|
+
// 以TOOL_CALLS_UNSUPPORTED回報而不假裝成功; 理由(工具迴圈須自建harness、tool_call
|
|
44
|
+
// 有會話束縛無法外傳上層agent)詳見dispatchApiOpenaiCompat.mjs與adapters.mjs檔頭。
|
|
45
|
+
//
|
|
46
|
+
// 【錯誤碼實測(Zen)】壞金鑰401(AuthError); 未知model亦回401(ModelError: Model X is not
|
|
47
|
+
// supported)而非404——故不可用狀態碼區分「金鑰錯」與「模型名錯」, 須讀stderr之訊息。
|
|
48
|
+
//
|
|
49
|
+
// 【結果結構對齊execCli】{ ok, stdout, stderr, code, error, errorType, durationMs,
|
|
50
|
+
// attempts, usage }, 與dispatchApiOpenaiCompat完全一致, 故dispatchAiFallback之
|
|
51
|
+
// 失敗分流與工作流層無須任何修改即可使用本kind。
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
//預設值
|
|
55
|
+
let DEFAULT_TIMEOUT_MS = dfTimeoutMs //全套件統一預設300000
|
|
56
|
+
let DEFAULT_RETRY_DELAY_MS = 5000
|
|
57
|
+
let MAX_RETRY_DELAY_MS = 15000
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
//optTruncate, 裁切失敗結果之內容時於刪節號後標註原始總長度(同execCli)
|
|
61
|
+
let optTruncate = {
|
|
62
|
+
funWithMsg: (str) => `(truncated, total ${str.length} chars)`,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 自Responses API之output陣列取出文字內容(僅取message型元素之output_text)
|
|
68
|
+
*
|
|
69
|
+
* reasoning型元素為模型思考過程(Zen實測其content為空陣列),不屬回覆內容故略過;
|
|
70
|
+
* 多個message元素依序串接
|
|
71
|
+
*
|
|
72
|
+
* @param {Array} output 輸入回應之output陣列
|
|
73
|
+
* @returns {String} 回傳串接後之文字內容,無有效內容回傳空字串
|
|
74
|
+
* @example
|
|
75
|
+
*
|
|
76
|
+
* import { extractOutputText } from './src/dispatchApiOpenaiResponses.mjs'
|
|
77
|
+
*
|
|
78
|
+
* let output = [
|
|
79
|
+
* { type: 'reasoning', content: [] },
|
|
80
|
+
* { type: 'message', content: [{ type: 'output_text', text: '完成' }] },
|
|
81
|
+
* ]
|
|
82
|
+
* console.log(extractOutputText(output))
|
|
83
|
+
* // => '完成'
|
|
84
|
+
*
|
|
85
|
+
*/
|
|
86
|
+
function extractOutputText(output) {
|
|
87
|
+
if (!isarr(output)) {
|
|
88
|
+
return ''
|
|
89
|
+
}
|
|
90
|
+
let rs = []
|
|
91
|
+
for (let item of output) {
|
|
92
|
+
if (get(item, 'type', '') !== 'message') {
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
let content = get(item, 'content', null)
|
|
96
|
+
if (!isarr(content)) {
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
for (let c of content) {
|
|
100
|
+
let t = get(c, 'text', null)
|
|
101
|
+
if (isestr(t)) {
|
|
102
|
+
rs.push(t)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return rs.join('')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 單次HTTP呼叫(內部使用, 不含重試邏輯)
|
|
112
|
+
*
|
|
113
|
+
* @param {String} url 輸入完整端點網址字串
|
|
114
|
+
* @param {Object} headers 輸入請求標頭物件
|
|
115
|
+
* @param {Object} body 輸入請求本體物件
|
|
116
|
+
* @param {Number} timeoutMs 輸入逾時毫秒
|
|
117
|
+
* @param {Function|null} validator 輸入驗證函式
|
|
118
|
+
* @returns {Promise} 回傳Promise,resolve回傳結果物件
|
|
119
|
+
*/
|
|
120
|
+
async function callOnce(url, headers, body, timeoutMs, validator) {
|
|
121
|
+
|
|
122
|
+
let t0 = Date.now()
|
|
123
|
+
|
|
124
|
+
//mkResult, 結果形狀之單一來源(欄位對齊execCli, 追加errorType與usage),
|
|
125
|
+
//durationMs於呼叫當下計算; 失敗分支各自給errorType, 成功分支不帶(僅失敗結果有此欄)
|
|
126
|
+
let mkResult = (patch) => ({
|
|
127
|
+
ok: false,
|
|
128
|
+
stdout: '',
|
|
129
|
+
stderr: '',
|
|
130
|
+
code: null,
|
|
131
|
+
error: '',
|
|
132
|
+
durationMs: Date.now() - t0,
|
|
133
|
+
usage: null,
|
|
134
|
+
...patch,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
//AbortController, 逾時中止(含回應本體之串流讀取)
|
|
138
|
+
let controller = new AbortController()
|
|
139
|
+
let timer = setTimeout(() => {
|
|
140
|
+
controller.abort()
|
|
141
|
+
}, timeoutMs)
|
|
142
|
+
|
|
143
|
+
let res = null
|
|
144
|
+
let txt = ''
|
|
145
|
+
try {
|
|
146
|
+
res = await fetch(url, {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers,
|
|
149
|
+
body: JSON.stringify(body),
|
|
150
|
+
signal: controller.signal,
|
|
151
|
+
})
|
|
152
|
+
txt = await res.text()
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
clearTimeout(timer)
|
|
156
|
+
|
|
157
|
+
//逾時, error以TIMEOUT開頭令dispatchAiFallback視為與金鑰無關而跳組
|
|
158
|
+
if (err.name === 'AbortError') {
|
|
159
|
+
return mkResult({ error: `TIMEOUT after ${timeoutMs / 1000}s`, errorType: 'timeout' })
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
//網路層錯誤(DNS/連線拒絕等)
|
|
163
|
+
let cause = get(err, 'cause.code', '') || err.message
|
|
164
|
+
return mkResult({ error: `FETCH_ERROR: ${cause}`, errorType: 'fetch' })
|
|
165
|
+
}
|
|
166
|
+
clearTimeout(timer)
|
|
167
|
+
|
|
168
|
+
//HTTP非2xx, 原始回應本體放stderr供除錯與分類
|
|
169
|
+
//(Zen實測: 壞金鑰與未知model皆401, 故訊息須讀stderr而非僅看狀態碼)
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
return mkResult({
|
|
172
|
+
stderr: strTruncate(txt, 1000, optTruncate),
|
|
173
|
+
code: res.status,
|
|
174
|
+
error: `HTTP ${res.status}`,
|
|
175
|
+
errorType: 'http',
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
//解析output/status/usage(token用量原樣透傳; 失敗回應亦可能已耗token, 一併帶出)
|
|
180
|
+
let output = null
|
|
181
|
+
let status = ''
|
|
182
|
+
let incompleteReason = ''
|
|
183
|
+
let failMsg = ''
|
|
184
|
+
let usage = null
|
|
185
|
+
let parsed = true
|
|
186
|
+
try {
|
|
187
|
+
let j = JSON.parse(txt)
|
|
188
|
+
output = get(j, 'output', null)
|
|
189
|
+
status = get(j, 'status', '')
|
|
190
|
+
incompleteReason = get(j, 'incomplete_details.reason', '')
|
|
191
|
+
failMsg = get(j, 'error.message', '') || get(j, 'error.code', '')
|
|
192
|
+
usage = get(j, 'usage', null)
|
|
193
|
+
if (!isobj(usage)) {
|
|
194
|
+
usage = null
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
parsed = false
|
|
199
|
+
}
|
|
200
|
+
if (!parsed || !isarr(output)) {
|
|
201
|
+
return mkResult({
|
|
202
|
+
stderr: strTruncate(txt, 500, optTruncate),
|
|
203
|
+
code: res.status,
|
|
204
|
+
error: 'INVALID_RESPONSE: missing output array',
|
|
205
|
+
errorType: 'invalid-response',
|
|
206
|
+
usage,
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
//function_call, 本轉接器不支援工具迴圈(見檔頭), 明確回報而不假裝成功
|
|
211
|
+
let hasToolCall = output.some((o) => get(o, 'type', '') === 'function_call')
|
|
212
|
+
if (hasToolCall) {
|
|
213
|
+
return mkResult({
|
|
214
|
+
stderr: strTruncate(txt, 1000, optTruncate),
|
|
215
|
+
code: res.status,
|
|
216
|
+
error: 'TOOL_CALLS_UNSUPPORTED: use a cli kind (opencode/claude/codex/antigravity) when tools are needed',
|
|
217
|
+
errorType: 'tool-unsupported',
|
|
218
|
+
usage,
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
//status非completed一律失敗: incomplete之內容為截斷品, 當成功回傳會讓半截結果流入下游
|
|
223
|
+
if (status !== 'completed') {
|
|
224
|
+
let detail = incompleteReason || failMsg || status || 'unknown'
|
|
225
|
+
return mkResult({
|
|
226
|
+
stdout: strTruncate(extractOutputText(output), 500, optTruncate),
|
|
227
|
+
stderr: strTruncate(txt, 500, optTruncate),
|
|
228
|
+
code: res.status,
|
|
229
|
+
error: `INCOMPLETE_RESPONSE: status=${status || 'missing'} (${detail})`,
|
|
230
|
+
errorType: 'incomplete',
|
|
231
|
+
usage,
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
//content, 自output取message型之output_text
|
|
236
|
+
let content = extractOutputText(output)
|
|
237
|
+
if (content === '') {
|
|
238
|
+
return mkResult({
|
|
239
|
+
stderr: strTruncate(txt, 500, optTruncate),
|
|
240
|
+
code: res.status,
|
|
241
|
+
error: 'INVALID_RESPONSE: no output_text in output messages',
|
|
242
|
+
errorType: 'invalid-response',
|
|
243
|
+
usage,
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
//validator, error與execCli一致令dispatchAiFallback可統一分流
|
|
248
|
+
if (validator && !validator(content)) {
|
|
249
|
+
return mkResult({
|
|
250
|
+
stdout: strTruncate(content, 500, optTruncate),
|
|
251
|
+
code: res.status,
|
|
252
|
+
error: 'OUTPUT_VALIDATION_FAILED',
|
|
253
|
+
errorType: 'validation',
|
|
254
|
+
usage,
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return mkResult({
|
|
259
|
+
ok: true,
|
|
260
|
+
stdout: content,
|
|
261
|
+
code: res.status,
|
|
262
|
+
usage,
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
//本轉接器不使用execCli, 全部設定鍵自理, 未知鍵一律忽略
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 以fetch直呼OpenAI Responses API(/responses)呼叫AI模型
|
|
272
|
+
*
|
|
273
|
+
* 特點:
|
|
274
|
+
* 免安裝CLI、免預先登入,給baseURL+key+model即可呼叫(如OpenCode Zen之muse-spark系與GPT系);
|
|
275
|
+
* 端點型別與dispatchApiOpenaiCompat不同——Zen之端點依模型家族而異,收錄前須查官方文件端點欄
|
|
276
|
+
* (https://opencode.ai/docs/zh-tw/zen/),打錯端點會得到HTTP 500而非404,詳見providers.mjs檔頭;
|
|
277
|
+
* 僅供純文字生成,需要工具能力請改用CLI類kind(opencode/claude/codex/antigravity);
|
|
278
|
+
* status非completed(如max_output_tokens耗盡)一律以INCOMPLETE_RESPONSE回報,不回半截內容;
|
|
279
|
+
* 結果結構與dispatchApiOpenaiCompat完全一致,可直接作為dispatchAi與dispatchAiFallback之kind('api-openai-responses')使用;
|
|
280
|
+
* 本函數不會reject,一律以結果物件之ok與error欄位回報成敗
|
|
281
|
+
*
|
|
282
|
+
* @param {String} prompt 輸入提示詞字串,作為input置於HTTP body
|
|
283
|
+
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
284
|
+
* @param {String} opt.baseURL 輸入API基底網址字串,例如'https://opencode.ai/zen/v1',將於尾端接上/responses
|
|
285
|
+
* @param {String} opt.model 輸入模型ID字串,例如'muse-spark-1.3-contributor-free'
|
|
286
|
+
* @param {String} [opt.key=''] 輸入API key字串,以Bearer置於Authorization標頭,預設''代表不帶認證標頭
|
|
287
|
+
* @param {String} [opt.system=''] 輸入system提示詞字串,將置於instructions欄位(Responses API之system管道),預設''代表不帶
|
|
288
|
+
* @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={}] 輸入額外請求標頭物件,預設{}
|
|
290
|
+
* @param {Number} [opt.timeoutMs=300000] 輸入逾時毫秒正整數,逾時將中止請求(含回應串流讀取),全套件統一預設300000
|
|
291
|
+
* @param {String|Function} [opt.validate=undefined] 輸入回覆內容驗證規則字串或自訂驗證函數,規則字串支援'nonempty'、'json'、'min:100',多規則可用逗號串接,預設undefined代表不驗證
|
|
292
|
+
* @param {Number} [opt.maxRetries=0] 輸入失敗後最大重試次數非負整數,4xx(429除外)不重試,預設0
|
|
293
|
+
* @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
|
|
295
|
+
* @example
|
|
296
|
+
* //need network, no cli required
|
|
297
|
+
*
|
|
298
|
+
* import dispatchApiOpenaiResponses from './src/dispatchApiOpenaiResponses.mjs'
|
|
299
|
+
*
|
|
300
|
+
* let test = async () => {
|
|
301
|
+
*
|
|
302
|
+
* //OpenCode Zen之muse-spark系走/responses(非chat/completions), 詳見providers.mjs檔頭
|
|
303
|
+
* let r = await dispatchApiOpenaiResponses('請只回覆兩個字:完成', {
|
|
304
|
+
* baseURL: 'https://opencode.ai/zen/v1',
|
|
305
|
+
* key: 'sk-xxxxxx',
|
|
306
|
+
* model: 'muse-spark-1.3-contributor-free',
|
|
307
|
+
* })
|
|
308
|
+
* console.log(r.ok, r.stdout.trim())
|
|
309
|
+
* // => true 完成
|
|
310
|
+
*
|
|
311
|
+
* let re = await dispatchApiOpenaiResponses('abc', { baseURL: 'https://opencode.ai/zen/v1', key: 'sk-bad', model: 'muse-spark-1.3-contributor-free' })
|
|
312
|
+
* console.log(re.ok, re.code, re.errorType)
|
|
313
|
+
* // => false 401 http
|
|
314
|
+
*
|
|
315
|
+
* }
|
|
316
|
+
* await test()
|
|
317
|
+
* .catch((err) => {
|
|
318
|
+
* console.log(err)
|
|
319
|
+
* })
|
|
320
|
+
*
|
|
321
|
+
*/
|
|
322
|
+
async function dispatchApiOpenaiResponses(prompt, opt = {}) {
|
|
323
|
+
|
|
324
|
+
//check prompt, 不reject故以錯誤結果物件回報
|
|
325
|
+
if (!isestr(prompt)) {
|
|
326
|
+
return getErrorResult('prompt must be a non-empty string')
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
//baseURL必填, API無CLI可回退
|
|
330
|
+
let baseURL = get(opt, 'baseURL', null)
|
|
331
|
+
if (!isestr(baseURL)) {
|
|
332
|
+
return getErrorResult('baseURL must be a non-empty string')
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
//model必填, responses無預設模型
|
|
336
|
+
let model = get(opt, 'model', null)
|
|
337
|
+
if (!isestr(model)) {
|
|
338
|
+
return getErrorResult('model must be a non-empty string')
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
//key, 無效代表不帶認證標頭(部分閘道免認證)
|
|
342
|
+
let key = get(opt, 'key', null)
|
|
343
|
+
|
|
344
|
+
//system, Responses API之system管道為instructions
|
|
345
|
+
let system = get(opt, 'system', null)
|
|
346
|
+
|
|
347
|
+
//bodyExtra
|
|
348
|
+
let bodyExtra = get(opt, 'body', null)
|
|
349
|
+
if (!isobj(bodyExtra)) {
|
|
350
|
+
bodyExtra = {}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
//headersExtra
|
|
354
|
+
let headersExtra = get(opt, 'headers', null)
|
|
355
|
+
if (!isobj(headersExtra)) {
|
|
356
|
+
headersExtra = {}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
//timeoutMs
|
|
360
|
+
let timeoutMs = castPintOr(get(opt, 'timeoutMs', null), DEFAULT_TIMEOUT_MS)
|
|
361
|
+
|
|
362
|
+
//maxRetries
|
|
363
|
+
let maxRetries = get(opt, 'maxRetries', null)
|
|
364
|
+
if (!isp0int(maxRetries)) {
|
|
365
|
+
maxRetries = 0
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
maxRetries = cint(maxRetries)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
//retryDelayMs
|
|
372
|
+
let retryDelayMs = castPintOr(get(opt, 'retryDelayMs', null), DEFAULT_RETRY_DELAY_MS)
|
|
373
|
+
|
|
374
|
+
//validator
|
|
375
|
+
let validator = buildValidator(get(opt, 'validate', null))
|
|
376
|
+
|
|
377
|
+
//url, baseURL尾端斜線正規化後接上端點
|
|
378
|
+
let url = baseURL.replace(/\/+$/, '') + '/responses'
|
|
379
|
+
|
|
380
|
+
//body, input為Responses API之輸入欄位; 額外鍵以bodyExtra為準(可覆寫temperature等)
|
|
381
|
+
let body = { model, input: prompt }
|
|
382
|
+
if (isestr(system)) {
|
|
383
|
+
body.instructions = system
|
|
384
|
+
}
|
|
385
|
+
body = { ...body, ...bodyExtra }
|
|
386
|
+
|
|
387
|
+
//headers
|
|
388
|
+
let headers = { 'Content-Type': 'application/json', ...headersExtra }
|
|
389
|
+
if (isestr(key)) {
|
|
390
|
+
headers['Authorization'] = `Bearer ${key}`
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let lastResult = null
|
|
394
|
+
let totalAttempts = 0
|
|
395
|
+
|
|
396
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
397
|
+
|
|
398
|
+
//delay, 重試間隔隨次數遞增, 上限15000ms(同execCli)
|
|
399
|
+
if (attempt > 0) {
|
|
400
|
+
await delay(Math.min(retryDelayMs * attempt, MAX_RETRY_DELAY_MS))
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
lastResult = await callOnce(url, headers, body, timeoutMs, validator)
|
|
404
|
+
totalAttempts = attempt + 1
|
|
405
|
+
|
|
406
|
+
if (lastResult.ok) {
|
|
407
|
+
lastResult.attempts = totalAttempts
|
|
408
|
+
return lastResult
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
//不可重試: 4xx(429除外)為客戶端錯誤, 重試無意義
|
|
412
|
+
let c = lastResult.code
|
|
413
|
+
if (isnum(c) && c >= 400 && c < 500 && c !== 429) {
|
|
414
|
+
break
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
lastResult.attempts = totalAttempts
|
|
420
|
+
|
|
421
|
+
return lastResult
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
export default dispatchApiOpenaiResponses
|
|
426
|
+
export { extractOutputText }
|
package/src/getErrorType.mjs
CHANGED
|
@@ -22,7 +22,9 @@ 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'
|
|
25
|
+
// 'invalid-response' 回應結構不合規(缺choices[0].message.content或缺output陣列, 僅api類)
|
|
26
|
+
// 'incomplete' Responses API之status非completed(如max_output_tokens耗盡而截斷,
|
|
27
|
+
// 僅api-openai-responses; 半截內容不當成功回傳, 見該轉接器檔頭)
|
|
26
28
|
// 'aborted' shouldStop中止(僅dispatchAiFallback)
|
|
27
29
|
// 'budget' 時間預算用盡(僅dispatchAiFallback)
|
|
28
30
|
|