w-dispatch-ai 1.0.2 → 1.0.4
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 +123 -35
- package/dist/w-dispatch-ai.umd.js +2 -2
- package/dist/w-dispatch-ai.umd.js.map +1 -1
- package/docs/WDispatchAi.mjs.html +8 -4
- package/docs/adapters.mjs.html +43 -7
- package/docs/dispatchAi.mjs.html +2 -2
- package/docs/dispatchAiFallback.mjs.html +42 -8
- package/docs/dispatchAiWkf.mjs.html +197 -0
- package/docs/dispatchAntigravity.mjs.html +2 -2
- package/docs/dispatchApiOpenaiCompat.mjs.html +516 -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/global.html +5768 -1469
- package/docs/index.html +2 -2
- package/docs/wkf_callAiWithFallback.mjs.html +282 -0
- package/docs/wkf_extractJsonLoose.mjs.html +180 -0
- package/docs/wkf_runFanout.mjs.html +227 -0
- package/docs/wkf_runFanoutPipeline.mjs.html +178 -0
- package/docs/wkf_runRolePipeline.mjs.html +195 -0
- package/g.mjs +41 -25
- package/package.json +1 -1
- package/src/WDispatchAi.mjs +6 -2
- package/src/adapters.mjs +41 -5
- package/src/dispatchAiFallback.mjs +40 -6
- package/src/dispatchAiWkf.mjs +125 -0
- package/src/dispatchApiOpenaiCompat.mjs +444 -0
- package/src/wkf/callAiWithFallback.mjs +210 -0
- package/src/wkf/extractJsonLoose.mjs +108 -0
- package/src/wkf/runFanout.mjs +155 -0
- package/src/wkf/runFanoutPipeline.mjs +106 -0
- package/src/wkf/runRolePipeline.mjs +123 -0
- package/test/tools/fakeServerForApiTest.mjs +151 -0
- package/test/unit-WDispatchAi.test.mjs +13 -6
- package/test/unit-adapters.test.mjs +5 -3
- package/test/unit-callAiWithFallback.test.mjs +146 -0
- package/test/unit-dispatchAi.test.mjs +1 -1
- package/test/unit-dispatchAiWkf.test.mjs +118 -0
- package/test/unit-dispatchApiOpenaiCompat.test.mjs +264 -0
- package/test/unit-extractJsonLoose.test.mjs +78 -0
- package/test/unit-runFanout.test.mjs +166 -0
- package/test/unit-runFanoutPipeline.test.mjs +86 -0
- package/test/unit-runRolePipeline.test.mjs +163 -0
package/g.mjs
CHANGED
|
@@ -27,7 +27,7 @@ let test = async () => {
|
|
|
27
27
|
|
|
28
28
|
//可用之AI供應商種類
|
|
29
29
|
console.log('KINDS:', wdi.KINDS)
|
|
30
|
-
// => KINDS: [ 'opencode', 'claude', 'codex', 'antigravity' ]
|
|
30
|
+
// => KINDS: [ 'opencode', 'claude', 'codex', 'antigravity', 'api-openai-compat' ]
|
|
31
31
|
|
|
32
32
|
let prompt = '請只回覆兩個字:完成,不要有任何其他文字'
|
|
33
33
|
|
|
@@ -51,6 +51,15 @@ let test = async () => {
|
|
|
51
51
|
console.log('antigravity:', r3b.ok, r3b.stdout.trim())
|
|
52
52
|
// => antigravity: true 完成
|
|
53
53
|
|
|
54
|
+
//以OpenAI相容API直呼(免CLI免登入), 給baseURL+key+model即可; Zen端點即opencode CLI之自家閘道
|
|
55
|
+
let r3c = await wdi.dispatchApiOpenaiCompat(prompt, {
|
|
56
|
+
baseURL: 'https://apihub.agnes-ai.com/v1',
|
|
57
|
+
key: agnesKeys[0],
|
|
58
|
+
model: 'agnes-2.0-flash',
|
|
59
|
+
})
|
|
60
|
+
console.log('api-openai-compat:', r3c.ok, r3c.code, r3c.stdout.trim())
|
|
61
|
+
// => api-openai-compat: true 200 完成
|
|
62
|
+
|
|
54
63
|
//以供應商條目輪替, 一個條目即一組(kind, model, 可選的key與provider與config), 輪到誰就用誰的CLI與模型
|
|
55
64
|
//opencode支援逐次注入金鑰, 故同一provider之多把金鑰可各成一個條目
|
|
56
65
|
let items = [
|
|
@@ -75,36 +84,43 @@ let test = async () => {
|
|
|
75
84
|
//未知供應商回傳error結果物件, 不會reject
|
|
76
85
|
let r4 = await wdi.dispatchAi('gemini', prompt)
|
|
77
86
|
console.log('invalid kind:', r4.ok, r4.error)
|
|
78
|
-
// => invalid kind: false unknown ai kind: "gemini" (available: opencode, claude, codex, antigravity)
|
|
87
|
+
// => invalid kind: false unknown ai kind: "gemini" (available: opencode, claude, codex, antigravity, api-openai-compat)
|
|
79
88
|
|
|
80
89
|
//prompt非有效字串亦回傳error結果物件
|
|
81
90
|
let r5 = await wdi.dispatchClaude('')
|
|
82
91
|
console.log('invalid prompt:', r5.ok, r5.error)
|
|
83
92
|
// => invalid prompt: false prompt must be a non-empty string
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
//執行失敗時, 由ok、code、error與stderr判斷原因
|
|
95
|
+
//REST路徑之錯誤依HTTP狀態碼分流(401金鑰無效、429限流、5xx服務端), 判別比CLI之stderr字串可靠
|
|
96
|
+
let r6 = await wdi.dispatchApiOpenaiCompat(prompt, {
|
|
97
|
+
baseURL: 'https://apihub.agnes-ai.com/v1',
|
|
98
|
+
model: 'agnes-2.0-flash',
|
|
89
99
|
key: 'sk-invalid-key',
|
|
90
100
|
})
|
|
91
|
-
console.log('invalid key:', r6.ok, r6.code, r6.error, r6.stderr.includes('
|
|
92
|
-
// => invalid key: false
|
|
101
|
+
console.log('invalid key:', r6.ok, r6.code, r6.error, r6.stderr.includes('无效的令牌'))
|
|
102
|
+
// => invalid key: false 401 HTTP 401 true
|
|
93
103
|
|
|
94
104
|
//多供應商自動遞補: providers順序即優先序, 組內keys以游標輪替
|
|
95
|
-
//此例第1把金鑰無效 → 自動換組內下一把成功;
|
|
105
|
+
//此例第1把金鑰無效 → 自動換組內下一把成功; 若整組用盡會遞補下一組, 依序往下
|
|
106
|
+
//
|
|
107
|
+
//【id命名】id為游標鍵與日誌標籤, 須區分到「模型」而非只到「廠商」——
|
|
108
|
+
// 取'claude'則日後無法同時掛sonnet與opus, 且日誌看不出實際用了哪個模型;
|
|
109
|
+
// 同一模型經不同路徑(REST/CLI/不同閘道)取得時額度池與故障域各自獨立,
|
|
110
|
+
// 屬不同供應商, 故id須帶上路徑前綴加以區分
|
|
96
111
|
let r7 = await wdi.dispatchAiFallback(prompt, {
|
|
97
112
|
providers: [
|
|
113
|
+
//REST版排前面: 免CLI、快3~5倍, 純文字任務優先走此路
|
|
98
114
|
{
|
|
99
|
-
id: '
|
|
100
|
-
kind: '
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
keys: ['sk-invalid-key-demo',
|
|
104
|
-
timeoutMs: 180000,
|
|
115
|
+
id: 'api:agnes-2.0-flash',
|
|
116
|
+
kind: 'api-openai-compat',
|
|
117
|
+
baseURL: 'https://apihub.agnes-ai.com/v1',
|
|
118
|
+
model: 'agnes-2.0-flash',
|
|
119
|
+
keys: ['sk-invalid-key-demo', agnesKeys[0]], //第1把無效, 示範組內輪替
|
|
105
120
|
},
|
|
121
|
+
//同一個agnes模型之CLI版: 有工具能力但較慢, 額度池亦不同, 屬另一個供應商
|
|
106
122
|
{
|
|
107
|
-
id: 'agnes',
|
|
123
|
+
id: 'oc:agnes-ai/agnes-2.0-flash',
|
|
108
124
|
kind: 'opencode',
|
|
109
125
|
model: 'agnes-ai/agnes-2.0-flash',
|
|
110
126
|
provider: 'agnes-ai',
|
|
@@ -112,21 +128,21 @@ let test = async () => {
|
|
|
112
128
|
config: configAgnes, //第三方provider須另給定義
|
|
113
129
|
timeoutMs: 180000,
|
|
114
130
|
},
|
|
115
|
-
{ id: 'claude', kind: 'claude', model: 'sonnet' },
|
|
116
|
-
{ id: 'codex', kind: 'codex', model: 'gpt-5.6-luna', sandbox: 'read-only' },
|
|
117
|
-
{ id: '
|
|
131
|
+
{ id: 'claude:sonnet', kind: 'claude', model: 'sonnet' },
|
|
132
|
+
{ id: 'codex:gpt-5.6-luna', kind: 'codex', model: 'gpt-5.6-luna', sandbox: 'read-only' },
|
|
133
|
+
{ id: 'agy:gemini-3.6-flash-low', kind: 'antigravity', model: 'gemini-3.6-flash-low' },
|
|
118
134
|
],
|
|
119
135
|
budgetMs: 600000,
|
|
120
136
|
onEvent: (ev) => console.log(' event:', ev.type, ev.keyId, ev.error || ''),
|
|
121
137
|
})
|
|
122
138
|
console.log('fallback:', r7.ok, r7.providerId, r7.keyIndex, r7.stdout.trim())
|
|
123
139
|
console.log('tried:', r7.tried.map((x) => `${x.keyId}:${x.outcome}`).join(', '))
|
|
124
|
-
// => event: try
|
|
125
|
-
// => event: next-key
|
|
126
|
-
// => event: try
|
|
127
|
-
// => event: ok
|
|
128
|
-
// => fallback: true
|
|
129
|
-
// => tried:
|
|
140
|
+
// => event: try api:agnes-2.0-flash#0
|
|
141
|
+
// => event: next-key api:agnes-2.0-flash#0 HTTP 401
|
|
142
|
+
// => event: try api:agnes-2.0-flash#1
|
|
143
|
+
// => event: ok api:agnes-2.0-flash#1
|
|
144
|
+
// => fallback: true api:agnes-2.0-flash 1 完成
|
|
145
|
+
// => tried: api:agnes-2.0-flash#0:next-key, api:agnes-2.0-flash#1:ok
|
|
130
146
|
|
|
131
147
|
}
|
|
132
148
|
await test()
|
package/package.json
CHANGED
package/src/WDispatchAi.mjs
CHANGED
|
@@ -2,10 +2,12 @@ import keys from 'lodash-es/keys.js'
|
|
|
2
2
|
import adapters from './adapters.mjs'
|
|
3
3
|
import dispatchAi from './dispatchAi.mjs'
|
|
4
4
|
import dispatchAiFallback from './dispatchAiFallback.mjs'
|
|
5
|
+
import dispatchAiWkf from './dispatchAiWkf.mjs'
|
|
5
6
|
import dispatchOpencode from './dispatchOpencode.mjs'
|
|
6
7
|
import dispatchClaude from './dispatchClaude.mjs'
|
|
7
8
|
import dispatchCodex from './dispatchCodex.mjs'
|
|
8
9
|
import dispatchAntigravity from './dispatchAntigravity.mjs'
|
|
10
|
+
import dispatchApiOpenaiCompat from './dispatchApiOpenaiCompat.mjs'
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
// WDispatchAi.mjs — AI供應商分派層
|
|
@@ -22,20 +24,22 @@ let KINDS = keys(adapters)
|
|
|
22
24
|
/**
|
|
23
25
|
* AI供應商分派
|
|
24
26
|
*
|
|
25
|
-
* @returns {Object} 回傳物件,其內含KINDS(可用供應商種類字串陣列)
|
|
27
|
+
* @returns {Object} 回傳物件,其內含KINDS(可用供應商種類字串陣列),dispatchAiWkf之工作流工廠函數,以及dispatchAi、dispatchAiFallback、dispatchOpencode、dispatchClaude、dispatchCodex、dispatchAntigravity、dispatchApiOpenaiCompat之async函數
|
|
26
28
|
* @example
|
|
27
29
|
*
|
|
28
|
-
* 詳見dispatchAi、dispatchAiFallback、dispatchOpencode、dispatchClaude、dispatchCodex、dispatchAntigravity範例
|
|
30
|
+
* 詳見dispatchAi、dispatchAiFallback、dispatchAiWkf、dispatchOpencode、dispatchClaude、dispatchCodex、dispatchAntigravity、dispatchApiOpenaiCompat範例
|
|
29
31
|
*
|
|
30
32
|
*/
|
|
31
33
|
let WDispatchAi = {
|
|
32
34
|
KINDS,
|
|
33
35
|
dispatchAi,
|
|
34
36
|
dispatchAiFallback,
|
|
37
|
+
dispatchAiWkf,
|
|
35
38
|
dispatchOpencode,
|
|
36
39
|
dispatchClaude,
|
|
37
40
|
dispatchCodex,
|
|
38
41
|
dispatchAntigravity,
|
|
42
|
+
dispatchApiOpenaiCompat,
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
|
package/src/adapters.mjs
CHANGED
|
@@ -2,6 +2,41 @@ import dispatchOpencode from './dispatchOpencode.mjs'
|
|
|
2
2
|
import dispatchClaude from './dispatchClaude.mjs'
|
|
3
3
|
import dispatchCodex from './dispatchCodex.mjs'
|
|
4
4
|
import dispatchAntigravity from './dispatchAntigravity.mjs'
|
|
5
|
+
import dispatchApiOpenaiCompat from './dispatchApiOpenaiCompat.mjs'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// adapters.mjs — kind對照表, 亦為「CLI或API」之選型判準所在
|
|
9
|
+
//
|
|
10
|
+
// ══ 選 kind 的唯一判準: 這次呼叫需不需要「工具」? ══
|
|
11
|
+
//
|
|
12
|
+
// 需要工具 → 用CLI類kind(opencode/claude/codex/antigravity)
|
|
13
|
+
// 所謂工具即: 讀取本機檔案、grep搜尋、執行shell指令、抓取網頁、寫檔。
|
|
14
|
+
// CLI本身是agentic harness, 自帶完整工具迴圈, 呼叫端什麼都不必做。
|
|
15
|
+
//
|
|
16
|
+
// 純文字生成 → 用API類kind(api-openai-compat)
|
|
17
|
+
// 所謂純文字即: 摘要、分析、改寫、翻譯、產出JSON——所有素材都已在prompt內,
|
|
18
|
+
// 模型只需讀prompt再輸出文字, 全程不需要碰外部世界。
|
|
19
|
+
// 免安裝CLI、免預先登入, 且實測比CLI快(agnes: API 1~2.5s vs CLI 4~6s)。
|
|
20
|
+
//
|
|
21
|
+
// ══ 為何API類不自建工具迴圈(2026-08-11實測後之決策, 勿再自行推翻) ══
|
|
22
|
+
//
|
|
23
|
+
// 1. 閘道端零內建工具: 實測Zen與Agnes皆然, 連web_search都沒有——
|
|
24
|
+
// 對`tools:[{type:'web_search'}]`回400並要求function.parameters,
|
|
25
|
+
// 即端點只接受「呼叫端自行定義且自行執行」的function工具。
|
|
26
|
+
// 2. 協定雖支援function calling(Zen之nemotron與Agnes皆實測回tool_calls),
|
|
27
|
+
// 但工具的定義、執行、錯誤處理、安全邊界全部得由本套件實作與維護,
|
|
28
|
+
// 等同重造CLI已經提供的harness; 工作流日後仍會持續擴充,
|
|
29
|
+
// 自建工具集之維護成本只會擴大, 故一律不走此路。
|
|
30
|
+
// 3. tool_calls有會話束縛(tool_call_id須於同一條messages串回填),
|
|
31
|
+
// 無法跨行程外傳給上層agent(如hermes)代為執行——
|
|
32
|
+
// 工作流是被上層阻塞呼叫的函式, 沒有反向請求工具的通道。
|
|
33
|
+
// 詳見dispatchApiOpenaiCompat.mjs檔頭。
|
|
34
|
+
//
|
|
35
|
+
// ══ 混用才是常態 ══
|
|
36
|
+
//
|
|
37
|
+
// 同一條dispatchAiFallback鏈可逐條目混搭kind, 工作流各階段亦然:
|
|
38
|
+
// 產生候選、整合收斂等純文字階段用API(快且省), 需要讀專案檔案或grep的
|
|
39
|
+
// 階段換CLI。判準永遠是「這一步需不需要碰外部世界」, 而非整條鏈二選一。
|
|
5
40
|
|
|
6
41
|
|
|
7
42
|
/**
|
|
@@ -16,14 +51,15 @@ import dispatchAntigravity from './dispatchAntigravity.mjs'
|
|
|
16
51
|
* import adapters from './src/adapters.mjs'
|
|
17
52
|
*
|
|
18
53
|
* console.log(Object.keys(adapters))
|
|
19
|
-
* // => ['opencode', 'claude', 'codex', 'antigravity']
|
|
54
|
+
* // => ['opencode', 'claude', 'codex', 'antigravity', 'api-openai-compat']
|
|
20
55
|
*
|
|
21
56
|
*/
|
|
22
57
|
let adapters = {
|
|
23
|
-
opencode: dispatchOpencode,
|
|
24
|
-
claude: dispatchClaude,
|
|
25
|
-
codex: dispatchCodex,
|
|
26
|
-
antigravity: dispatchAntigravity,
|
|
58
|
+
'opencode': dispatchOpencode,
|
|
59
|
+
'claude': dispatchClaude,
|
|
60
|
+
'codex': dispatchCodex,
|
|
61
|
+
'antigravity': dispatchAntigravity,
|
|
62
|
+
'api-openai-compat': dispatchApiOpenaiCompat,
|
|
27
63
|
}
|
|
28
64
|
|
|
29
65
|
|
|
@@ -28,6 +28,32 @@ import getErrorResult from './getErrorResult.mjs'
|
|
|
28
28
|
//
|
|
29
29
|
// 【時間預算】budgetMs限制整輪遞補的總時長, 剩餘預算會壓進每次呼叫的timeoutMs,
|
|
30
30
|
// 防止多家連續卡逾時而撞破外部排程的執行上限。
|
|
31
|
+
//
|
|
32
|
+
// ══ 條目id之設計規則(呼叫端負責, 本套件不解讀其內容) ══
|
|
33
|
+
//
|
|
34
|
+
// id於本套件內只有兩個用途: 游標的物件鍵(state.cursors[id])與日誌標籤
|
|
35
|
+
// (providerId、keyId=`${id}#${keyIndex}`)。不查表、不比對、無格式要求,
|
|
36
|
+
// 純粹是呼叫端的命名空間——故「什麼算同一個供應商」由呼叫端定義, 本套件不猜。
|
|
37
|
+
//
|
|
38
|
+
// ① id須能區分到「模型」而非只到「廠商」
|
|
39
|
+
// ✗ id:'claude' —— 日後要同時掛sonnet與opus就無法並存, 且日誌看不出用了哪個模型
|
|
40
|
+
// ✓ id:'claude:sonnet' / id:'claude:opus'
|
|
41
|
+
//
|
|
42
|
+
// ② 同一模型經不同路徑取得時, id須帶上路徑
|
|
43
|
+
// 同一個laguna可經Poolside官方REST、OpenRouter、opencode CLI三條路,
|
|
44
|
+
// 三者額度池與故障域各自獨立, 屬三個供應商:
|
|
45
|
+
// ✓ 'poolside:laguna-s-2.1' / 'or:poolside/laguna-s-2.1:free' / 'oc:poolside/poolside/laguna-s-2.1'
|
|
46
|
+
//
|
|
47
|
+
// ③ id務必給且務必唯一
|
|
48
|
+
// 未給時本套件回退為「陣列索引字串」——索引是位置不是身分, 日後於鏈中插入條目
|
|
49
|
+
// 會讓後續條目繼承他人的游標進度(輪替張冠李戴), 故正式設定一律明給。
|
|
50
|
+
// 兩個條目同id則共用同一游標且日誌無法區分, 屬設定錯誤。
|
|
51
|
+
//
|
|
52
|
+
// ④ 同一組金鑰用於多個條目時, 各條目游標獨立
|
|
53
|
+
// 例如agnes的CLI版與REST版共用同一批金鑰時, 兩者各自從游標起點輪替,
|
|
54
|
+
// 同一把金鑰可能被連續使用而另一把閒置(帳號額度未均攤)。
|
|
55
|
+
// 要讓它們共享輪替進度就給相同id(代價: 日誌無法區分兩者);
|
|
56
|
+
// 要能區分就分開命名(代價: 額度不均攤)。此取捨由呼叫端依實際需求決定。
|
|
31
57
|
|
|
32
58
|
|
|
33
59
|
//fallback層自用之設定鍵, 其餘鍵作為各attempt之共用預設原樣轉傳
|
|
@@ -106,7 +132,7 @@ function isKeyIndependentFail(r) {
|
|
|
106
132
|
* @param {String} prompt 輸入提示詞字串,一律以stdin傳入子進程
|
|
107
133
|
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
108
134
|
* @param {Array} opt.providers 輸入供應商條目物件陣列,順序即優先序。各條目除下列鍵外,其餘鍵(kind、model、exe、provider、config、sandbox、timeoutMs等)即該條目之opt原樣透傳對應轉接器
|
|
109
|
-
* @param {String} [opt.providers[].id=條目索引字串]
|
|
135
|
+
* @param {String} [opt.providers[].id=條目索引字串] 輸入群組識別字串,游標以此為鍵、亦為日誌標籤,本套件不解讀其內容。須區分到「模型」而非只到「廠商」(如'claude:sonnet'而非'claude'),同一模型經不同路徑取得時須帶上路徑(如'poolside:laguna-s-2.1'與'or:poolside/laguna-s-2.1:free'),且務必唯一。省略時回退為陣列索引字串——索引是位置不是身分,日後插入條目會令後續條目繼承他人游標進度,故正式設定一律明給。詳見本檔檔頭之id設計規則
|
|
110
136
|
* @param {Array} [opt.providers[].keys=[]] 輸入同一服務之多把API key字串陣列,逐次注入輪替(kind為opencode時須同時於條目給予provider),省略代表沿用CLI既有登入狀態之單一虛擬金鑰
|
|
111
137
|
* @param {Number} [opt.budgetMs=null] 輸入整輪遞補之時間上限毫秒正整數,剩餘預算會壓進每次呼叫之timeoutMs,預設null代表不限
|
|
112
138
|
* @param {Number} [opt.minAttemptMs=20000] 輸入單次嘗試之最低剩餘預算毫秒正整數,剩餘低於此值即停止嘗試回報budget exhausted,預設20000
|
|
@@ -126,21 +152,29 @@ function isKeyIndependentFail(r) {
|
|
|
126
152
|
* let r = await dispatchAiFallback('請只回覆兩個字:完成', {
|
|
127
153
|
* providers: [
|
|
128
154
|
* {
|
|
129
|
-
* id
|
|
155
|
+
* //id區分到模型且帶路徑: 同一模型經REST與CLI取得屬兩個供應商
|
|
156
|
+
* id: 'zen:deepseek-v4-flash-free',
|
|
157
|
+
* kind: 'api-openai-compat',
|
|
158
|
+
* baseURL: 'https://opencode.ai/zen/v1',
|
|
159
|
+
* model: 'deepseek-v4-flash-free',
|
|
160
|
+
* keys: ['sk-aaa', 'sk-bbb'], //多把金鑰, 某把失敗自動換下一把
|
|
161
|
+
* },
|
|
162
|
+
* {
|
|
163
|
+
* id: 'oc:opencode/deepseek-v4-flash-free', //同一模型之CLI版(有工具, 較慢)
|
|
130
164
|
* kind: 'opencode',
|
|
131
165
|
* model: 'opencode/deepseek-v4-flash-free',
|
|
132
166
|
* provider: 'opencode',
|
|
133
|
-
* keys: ['sk-aaa', 'sk-bbb'],
|
|
167
|
+
* keys: ['sk-aaa', 'sk-bbb'],
|
|
134
168
|
* timeoutMs: 180000,
|
|
135
169
|
* },
|
|
136
|
-
* { id: 'claude', kind: 'claude', model: 'sonnet' },
|
|
137
|
-
* { id: 'codex', kind: 'codex', model: 'gpt-5.6-luna', sandbox: 'read-only' },
|
|
170
|
+
* { id: 'claude:sonnet', kind: 'claude', model: 'sonnet' }, //以上全敗時遞補
|
|
171
|
+
* { id: 'codex:gpt-5.6-luna', kind: 'codex', model: 'gpt-5.6-luna', sandbox: 'read-only' },
|
|
138
172
|
* ],
|
|
139
173
|
* budgetMs: 600000,
|
|
140
174
|
* onEvent: (ev) => console.log(ev.type, ev.providerId, ev.keyIndex),
|
|
141
175
|
* })
|
|
142
176
|
* console.log(r.ok, r.providerId, r.keyIndex, r.tried.length)
|
|
143
|
-
* // => true 'deepseek' 0 1
|
|
177
|
+
* // => true 'zen:deepseek-v4-flash-free' 0 1
|
|
144
178
|
*
|
|
145
179
|
* }
|
|
146
180
|
* await test()
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import get from 'lodash-es/get.js'
|
|
2
|
+
import isobj from 'wsemi/src/isobj.mjs'
|
|
3
|
+
import callAiWithFallback from './wkf/callAiWithFallback.mjs'
|
|
4
|
+
import runFanout from './wkf/runFanout.mjs'
|
|
5
|
+
import runRolePipeline from './wkf/runRolePipeline.mjs'
|
|
6
|
+
import runFanoutPipeline from './wkf/runFanoutPipeline.mjs'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
// dispatchAiWkf.mjs — 工作流工廠: 注入provider定義表與共用預設, 回傳綁定版API
|
|
10
|
+
//
|
|
11
|
+
// 【用途】專案端只需注入一次providers(名稱 → dispatchAiFallback條目)與共用設定
|
|
12
|
+
// (cwd、store、onEvent、timeoutMs…), 之後以名稱宣告工作流即可, 不必每次傳定義表。
|
|
13
|
+
//
|
|
14
|
+
// 【並行與游標之說明】多名額並行且共用同一store時, 游標read-modify-write
|
|
15
|
+
// 可能交錯, 造成金鑰輪替不完全均攤——只影響公平性、不影響正確性(每把金鑰仍有效),
|
|
16
|
+
// 故不加鎖; 要求嚴格均攤者可注入自帶佇列的store。
|
|
17
|
+
//
|
|
18
|
+
// 【本函數為同步工廠會throw】providers無效屬設定錯誤, 應於啟動期即失敗(fail fast),
|
|
19
|
+
// 與各dispatch函數「不reject」之約定不衝突——後者是執行期呼叫, 前者是組裝期設定。
|
|
20
|
+
//
|
|
21
|
+
// 【定義表之鍵名即條目id】該鍵名會成為dispatchAiFallback之條目id(游標鍵與日誌標籤),
|
|
22
|
+
// 須區分到「模型」而非只到「廠商」——鍵名取'claude'則日後無法同時掛sonnet與opus,
|
|
23
|
+
// 且日誌看不出實際用了哪個模型; 同一模型經不同路徑(REST/CLI/不同閘道)取得時,
|
|
24
|
+
// 額度池與故障域各自獨立而屬不同供應商, 鍵名須帶上路徑加以區分。
|
|
25
|
+
// 命名規則與取捨詳見dispatchAiFallback.mjs檔頭之「條目id之設計規則」。
|
|
26
|
+
//
|
|
27
|
+
// 【定義providers時如何選kind: CLI或API】判準是「該階段需不需要工具」:
|
|
28
|
+
// 需要讀本機檔案、grep、執行指令、抓網頁 → CLI類kind(opencode/claude/codex/antigravity);
|
|
29
|
+
// 純文字生成(素材皆已在prompt內) → API類kind(api-openai-compat), 免安裝免登入且較快。
|
|
30
|
+
// 工作流常態是混用: runFanout之候選生成與整合、runRolePipeline之審計修訂等
|
|
31
|
+
// 多屬純文字階段可走API; 唯獨需要實際翻閱專案檔案的階段必須走CLI。
|
|
32
|
+
// API類不支援工具且不會自建工具迴圈, 理由詳見adapters.mjs檔頭之選型判準區塊。
|
|
33
|
+
//
|
|
34
|
+
// 【工具無法向上層轉送】工作流各名額(如runFanout之agents)只是同行程之async函數呼叫,
|
|
35
|
+
// 非獨立agent; 模型回傳之tool_calls受會話束縛而無法外傳給上層agent(如hermes)代跑,
|
|
36
|
+
// 故「讓外殼提供工具給工作流內的模型使用」在本架構下不成立——需要工具就選CLI類kind。
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 建立AI工作流執行環境(工廠),注入provider定義表與共用預設後回傳綁定版API
|
|
41
|
+
*
|
|
42
|
+
* 特點:
|
|
43
|
+
* providers為名稱對dispatchAiFallback條目之定義表,之後各工作流以名稱宣告主模型與遞補鏈;
|
|
44
|
+
* defaults為共用呼叫設定,各工作流之callOpt與名額規格可逐項覆寫;
|
|
45
|
+
* 回傳之各函數皆不reject;本工廠為同步函數,providers無效時throw(設定錯誤應於啟動期即失敗)
|
|
46
|
+
*
|
|
47
|
+
* @param {Object} opt 輸入設定物件
|
|
48
|
+
* @param {Object} opt.providers 輸入provider定義表物件(名稱 → dispatchAiFallback條目:{ kind, model, keys, exe, provider, config, sandbox, extraArgs... })
|
|
49
|
+
* @param {Object} [opt.defaults={}] 輸入共用呼叫設定物件(cwd、store、onEvent、timeoutMs、budgetMs、maxRetries、promptPrefix、parse等),預設{}
|
|
50
|
+
* @returns {Object} 回傳綁定版API物件,內含callAi(單一名額呼叫)、runFanout(多開+整合)、runRolePipeline(串行角色鏈)、runFanoutPipeline(多開+整合+角色鏈)、providers(定義表原樣)
|
|
51
|
+
* @example
|
|
52
|
+
* //need cli in system PATH
|
|
53
|
+
*
|
|
54
|
+
* import dispatchAiWkf from './src/dispatchAiWkf.mjs'
|
|
55
|
+
*
|
|
56
|
+
* //定義表之鍵名即dispatchAiFallback之條目id, 須區分到模型而非只到廠商,
|
|
57
|
+
* //同一模型經不同路徑取得時須帶上路徑(REST與CLI屬兩個供應商, 能力與速度皆不同)
|
|
58
|
+
* let wkf = dispatchAiWkf({
|
|
59
|
+
* providers: {
|
|
60
|
+
* 'zen:deepseek-v4-flash-free': { kind: 'api-openai-compat', baseURL: 'https://opencode.ai/zen/v1', model: 'deepseek-v4-flash-free', keys: ['sk-xxx'] },
|
|
61
|
+
* 'oc:opencode/deepseek-v4-flash-free': { kind: 'opencode', model: 'opencode/deepseek-v4-flash-free', provider: 'opencode', keys: ['sk-xxx'] },
|
|
62
|
+
* 'claude:sonnet': { kind: 'claude', model: 'sonnet' },
|
|
63
|
+
* 'claude:opus': { kind: 'claude', model: 'opus' },
|
|
64
|
+
* 'codex:gpt-5.6-luna': { kind: 'codex', model: 'gpt-5.6-luna' },
|
|
65
|
+
* },
|
|
66
|
+
* defaults: { timeoutMs: 300000 },
|
|
67
|
+
* })
|
|
68
|
+
*
|
|
69
|
+
* let test = async () => {
|
|
70
|
+
*
|
|
71
|
+
* //單一名額: 主模型+遞補鏈
|
|
72
|
+
* let r1 = await wkf.callAi('只回覆JSON: {"a":1}', { spec: { use: 'zen:deepseek-v4-flash-free', fallback: ['claude:sonnet'] }, check: (j) => j.a === 1 })
|
|
73
|
+
* console.log(r1.ok, r1.json)
|
|
74
|
+
* // => true { a: 1 }
|
|
75
|
+
*
|
|
76
|
+
* //Fanout工作流: 多開執行+單點整合
|
|
77
|
+
* //純文字階段用REST(快), 需要讀專案檔案之階段才用CLI(有工具)
|
|
78
|
+
* let r2 = await wkf.runFanout({
|
|
79
|
+
* task: '分析並只回覆JSON: {"essence":"..."}',
|
|
80
|
+
* agents: [
|
|
81
|
+
* { use: 'zen:deepseek-v4-flash-free', fallback: ['claude:sonnet'] },
|
|
82
|
+
* { use: 'claude:sonnet' },
|
|
83
|
+
* ],
|
|
84
|
+
* integrate: { use: 'codex:gpt-5.6-luna' },
|
|
85
|
+
* check: (j) => !!j.essence,
|
|
86
|
+
* })
|
|
87
|
+
* console.log(r2.ok, r2.integrated)
|
|
88
|
+
* // => true true
|
|
89
|
+
*
|
|
90
|
+
* }
|
|
91
|
+
* await test()
|
|
92
|
+
* .catch((err) => {
|
|
93
|
+
* console.log(err)
|
|
94
|
+
* })
|
|
95
|
+
*
|
|
96
|
+
*/
|
|
97
|
+
function dispatchAiWkf(opt = {}) {
|
|
98
|
+
let providers = get(opt, 'providers', null)
|
|
99
|
+
if (!isobj(providers)) {
|
|
100
|
+
throw new Error('dispatchAiWkf: opt.providers must be an object (name → provider entry)')
|
|
101
|
+
}
|
|
102
|
+
let defaults = get(opt, 'defaults', null)
|
|
103
|
+
if (!isobj(defaults)) {
|
|
104
|
+
defaults = {}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
providers,
|
|
109
|
+
|
|
110
|
+
//單一名額呼叫: callAi(prompt, { spec:{use,fallback}, check, ... })
|
|
111
|
+
callAi: (prompt, o = {}) => callAiWithFallback(prompt, { ...defaults, ...o, providers }),
|
|
112
|
+
|
|
113
|
+
//Fanout工作流: runFanout({ task, agents, integrate, check, schema, minCandidates, callOpt? })
|
|
114
|
+
runFanout: (o = {}) => runFanout({ ...o, providers, callOpt: { ...defaults, ...get(o, 'callOpt', {}) } }),
|
|
115
|
+
|
|
116
|
+
//RolePipeline工作流: runRolePipeline({ input, stages, callOpt? })
|
|
117
|
+
runRolePipeline: (o = {}) => runRolePipeline({ ...o, providers, callOpt: { ...defaults, ...get(o, 'callOpt', {}) } }),
|
|
118
|
+
|
|
119
|
+
//FanoutPipeline工作流: runFanoutPipeline({ task, agents, integrate, stages, check, schema, callOpt? })
|
|
120
|
+
runFanoutPipeline: (o = {}) => runFanoutPipeline({ ...o, providers, callOpt: { ...defaults, ...get(o, 'callOpt', {}) } }),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
export default dispatchAiWkf
|