w-dispatch-ai 1.0.3 → 1.0.5

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.
Files changed (50) hide show
  1. package/README.md +153 -43
  2. package/dist/w-dispatch-ai.umd.js +2 -2
  3. package/dist/w-dispatch-ai.umd.js.map +1 -1
  4. package/docs/WDispatchAi.mjs.html +8 -4
  5. package/docs/adapters.mjs.html +36 -2
  6. package/docs/dfTimeoutMs.mjs.html +98 -0
  7. package/docs/dispatchAi.mjs.html +2 -2
  8. package/docs/dispatchAiFallback.mjs.html +55 -16
  9. package/docs/dispatchAiWkf.mjs.html +42 -8
  10. package/docs/dispatchAntigravity.mjs.html +23 -12
  11. package/docs/dispatchApiOpenaiCompat.mjs.html +40 -6
  12. package/docs/dispatchClaude.mjs.html +17 -4
  13. package/docs/dispatchCodex.mjs.html +16 -3
  14. package/docs/dispatchOpencode.mjs.html +16 -3
  15. package/docs/getCliArgs.mjs.html +2 -2
  16. package/docs/getErrorResult.mjs.html +2 -2
  17. package/docs/global.html +511 -66
  18. package/docs/index.html +2 -2
  19. package/docs/resolveProviders.mjs.html +189 -0
  20. package/docs/wkf_callAiWithFallback.mjs.html +22 -11
  21. package/docs/wkf_extractJsonLoose.mjs.html +2 -2
  22. package/docs/wkf_runFanout.mjs.html +7 -7
  23. package/docs/wkf_runFanoutPipeline.mjs.html +7 -7
  24. package/docs/wkf_runRolePipeline.mjs.html +6 -6
  25. package/g.mjs +30 -23
  26. package/package.json +1 -1
  27. package/src/WDispatchAi.mjs +6 -2
  28. package/src/adapters.mjs +34 -0
  29. package/src/dfTimeoutMs.mjs +26 -0
  30. package/src/dispatchAiFallback.mjs +53 -14
  31. package/src/dispatchAiWkf.mjs +40 -6
  32. package/src/dispatchAntigravity.mjs +21 -10
  33. package/src/dispatchApiOpenaiCompat.mjs +38 -4
  34. package/src/dispatchClaude.mjs +15 -2
  35. package/src/dispatchCodex.mjs +14 -1
  36. package/src/dispatchOpencode.mjs +14 -1
  37. package/src/providers.mjs +133 -0
  38. package/src/resolveProviders.mjs +117 -0
  39. package/src/wkf/callAiWithFallback.mjs +20 -9
  40. package/src/wkf/runFanout.mjs +5 -5
  41. package/src/wkf/runFanoutPipeline.mjs +5 -5
  42. package/src/wkf/runRolePipeline.mjs +4 -4
  43. package/test/tools/fakeServerForApiTest.mjs +14 -0
  44. package/test/unit-WDispatchAi.test.mjs +9 -3
  45. package/test/unit-dispatchAi.test.mjs +1 -1
  46. package/test/unit-dispatchAiFallback.test.mjs +25 -0
  47. package/test/unit-dispatchAntigravity.test.mjs +25 -5
  48. package/test/unit-dispatchApiOpenaiCompat.test.mjs +31 -0
  49. package/test/unit-providers.test.mjs +66 -0
  50. package/test/unit-resolveProviders.test.mjs +85 -0
@@ -0,0 +1,66 @@
1
+ import assert from 'assert'
2
+ import keys from 'lodash-es/keys.js'
3
+ import providers from '../src/providers.mjs'
4
+ import adapters from '../src/adapters.mjs'
5
+
6
+
7
+ describe('providers', function() {
8
+
9
+ it('預設匯出為非空條目陣列', function() {
10
+ let r = [Array.isArray(providers), providers.length > 0]
11
+ let rr = [true, true]
12
+ assert.strict.deepEqual(r, rr)
13
+ })
14
+
15
+ it('各條目皆有非空字串之id、model、kind', function() {
16
+ let r = providers.map((p) => [typeof p.id === 'string' && p.id !== '', typeof p.model === 'string' && p.model !== '', typeof p.kind === 'string' && p.kind !== ''])
17
+ let rr = providers.map(() => [true, true, true])
18
+ assert.strict.deepEqual(r, rr)
19
+ })
20
+
21
+ it('各條目之kind皆為有效供應商種類', function() {
22
+ let ks = keys(adapters)
23
+ let r = providers.map((p) => [p.id, ks.includes(p.kind)])
24
+ let rr = providers.map((p) => [p.id, true])
25
+ assert.strict.deepEqual(r, rr)
26
+ })
27
+
28
+ it('id不重複且皆符合「路徑:模型」命名(含冒號)', function() {
29
+ let ids = providers.map((p) => p.id)
30
+ let r = [new Set(ids).size === ids.length, ids.every((id) => id.includes(':'))]
31
+ let rr = [true, true]
32
+ assert.strict.deepEqual(r, rr)
33
+ })
34
+
35
+ it('api-openai-compat條目皆有baseURL', function() {
36
+ let apis = providers.filter((p) => p.kind === 'api-openai-compat')
37
+ let r = [apis.length > 0, apis.every((p) => typeof p.baseURL === 'string' && p.baseURL.startsWith('http'))]
38
+ let rr = [true, true]
39
+ assert.strict.deepEqual(r, rr)
40
+ })
41
+
42
+ it('opencode條目皆有provider(金鑰注入所需之配對)', function() {
43
+ let ocs = providers.filter((p) => p.kind === 'opencode')
44
+ let r = [ocs.length > 0, ocs.every((p) => typeof p.provider === 'string' && p.provider !== '')]
45
+ let rr = [true, true]
46
+ assert.strict.deepEqual(r, rr)
47
+ })
48
+
49
+ it('envVar命名皆為大寫底線格式, 訂閱登入態條目(claude/codex)無envVar', function() {
50
+ let withVar = providers.filter((p) => p.envVar !== undefined)
51
+ let subs = providers.filter((p) => ['claude', 'codex'].includes(p.kind))
52
+ let r = [
53
+ withVar.every((p) => /^[A-Z][A-Z0-9_]*$/.test(p.envVar)),
54
+ subs.every((p) => p.envVar === undefined),
55
+ ]
56
+ let rr = [true, true]
57
+ assert.strict.deepEqual(r, rr)
58
+ })
59
+
60
+ it('條目不自帶timeoutMs(由上層依任務型態統一給定)', function() {
61
+ let r = providers.every((p) => p.timeoutMs === undefined)
62
+ let rr = true
63
+ assert.strict.deepEqual(r, rr)
64
+ })
65
+
66
+ })
@@ -0,0 +1,85 @@
1
+ import assert from 'assert'
2
+ import resolveProviders from '../src/resolveProviders.mjs'
3
+ import providersAll from '../src/providers.mjs'
4
+
5
+
6
+ describe('resolveProviders', function() {
7
+
8
+ //測試用定義, 不依賴真實環境變數
9
+ let defs = [
10
+ { id: 'api:m1', kind: 'api-openai-compat', baseURL: 'https://x/v1', model: 'm1', envVar: 'T_K1' },
11
+ { id: 'api:m2', kind: 'api-openai-compat', baseURL: 'https://x/v1', model: 'm2', envVar: 'T_K2' },
12
+ { id: 'claude:sonnet', kind: 'claude', model: 'sonnet' },
13
+ { id: 'api:m3', kind: 'api-openai-compat', baseURL: 'https://x/v1', model: 'm3', envVar: 'T_K1', keys: ['sk-own'] },
14
+ ]
15
+ let env = { T_K1: 'sk-a, sk-b,', T_K2: '' }
16
+
17
+ it('envVar展開為keys(逗號分隔且修剪空白), envVar欄位自輸出移除', function() {
18
+ let { providers } = resolveProviders(defs, { env })
19
+ let p = providers.find((x) => x.id === 'api:m1')
20
+ let r = [p.keys, p.envVar]
21
+ let rr = [['sk-a', 'sk-b'], undefined]
22
+ assert.strict.deepEqual(r, rr)
23
+ })
24
+
25
+ it('缺環境變數(或值為空)之條目停用並列入skipped', function() {
26
+ let { providers, skipped } = resolveProviders(defs, { env })
27
+ let r = [providers.map((x) => x.id), skipped]
28
+ let rr = [['api:m1', 'claude:sonnet', 'api:m3'], [{ id: 'api:m2', envVar: 'T_K2' }]]
29
+ assert.strict.deepEqual(r, rr)
30
+ })
31
+
32
+ it('無envVar之條目(訂閱登入態)原樣通過, 自帶keys者以keys為準', function() {
33
+ let { providers } = resolveProviders(defs, { env })
34
+ let pc = providers.find((x) => x.id === 'claude:sonnet')
35
+ let p3 = providers.find((x) => x.id === 'api:m3')
36
+ let r = [pc.model, p3.keys, p3.envVar]
37
+ let rr = ['sonnet', ['sk-own'], undefined]
38
+ assert.strict.deepEqual(r, rr)
39
+ })
40
+
41
+ it('pick自選子集且依pick順序回傳, 查無之id列入missing', function() {
42
+ let { providers, missing } = resolveProviders(defs, { env, pick: ['claude:sonnet', 'api:m1', 'api:nope'] })
43
+ let r = [providers.map((x) => x.id), missing]
44
+ let rr = [['claude:sonnet', 'api:m1'], ['api:nope']]
45
+ assert.strict.deepEqual(r, rr)
46
+ })
47
+
48
+ it('table以id為鍵, 可直接作dispatchAiWkf之定義表', function() {
49
+ let { table } = resolveProviders(defs, { env })
50
+ let r = [Object.keys(table), table['api:m1'].keys]
51
+ let rr = [['api:m1', 'claude:sonnet', 'api:m3'], ['sk-a', 'sk-b']]
52
+ assert.strict.deepEqual(r, rr)
53
+ })
54
+
55
+ it('輸入陣列與條目皆不被改動', function() {
56
+ let before = JSON.stringify(defs)
57
+ resolveProviders(defs, { env })
58
+ let r = JSON.stringify(defs)
59
+ assert.strict.deepEqual(r, before)
60
+ })
61
+
62
+ it('providers無效時回傳空結果不throw', function() {
63
+ let r = []
64
+ for (let v of [null, undefined, 'abc', {}, [null, 'x']]) {
65
+ let t = resolveProviders(v, { env })
66
+ r.push([t.providers.length, t.skipped.length, t.missing.length])
67
+ }
68
+ let rr = [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]
69
+ assert.strict.deepEqual(r, rr)
70
+ })
71
+
72
+ it('可展開套件內建providers.mjs(以模擬env驗證, 不依賴真實.env)', function() {
73
+ let fakeEnv = { OPENCODE_KEYS: 'sk-1,sk-2', AGNES_KEYS: 'sk-3', POOLSIDE_KEYS: 'sk-4' }
74
+ let { providers, skipped } = resolveProviders(providersAll, { env: fakeEnv })
75
+ let r = [
76
+ providers.length === providersAll.length, //三個envVar皆有值 → 無停用
77
+ skipped.length,
78
+ providers.every((p) => p.envVar === undefined),
79
+ providers.filter((p) => Array.isArray(p.keys)).length > 0,
80
+ ]
81
+ let rr = [true, 0, true, true]
82
+ assert.strict.deepEqual(r, rr)
83
+ })
84
+
85
+ })