w-dispatch-ai 1.0.37 → 1.0.39
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 +24 -6
- 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 +2 -2
- 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 +37 -5
- package/docs/dispatchAiWkf.mjs.html +2 -2
- package/docs/dispatchAntigravity.mjs.html +2 -2
- package/docs/dispatchApiOpenaiCompat.mjs.html +33 -7
- package/docs/dispatchApiOpenaiResponses.mjs.html +13 -4
- package/docs/dispatchApiTypesafeSystemone.mjs.html +21 -4
- 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 +255 -21
- 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 +4 -3
- 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 +5 -3
- package/package.json +1 -1
- package/src/describeNonJsonBody.mjs +50 -0
- package/src/dispatchAiFallback.mjs +35 -3
- package/src/dispatchApiOpenaiCompat.mjs +31 -5
- package/src/dispatchApiOpenaiResponses.mjs +11 -2
- package/src/dispatchApiTypesafeSystemone.mjs +19 -2
- package/src/getErrorType.mjs +3 -1
- package/src/wkf/callAiWithFallback.mjs +2 -1
- package/src/wkf/salvageTruncatedArray.mjs +3 -1
- package/test/tools/fakeServerForApiTest.mjs +17 -2
- package/test/unit-dispatchAiFallback.test.mjs +245 -2
- package/test/unit-dispatchApiOpenaiCompat.test.mjs +41 -3
- package/test/unit-dispatchApiOpenaiResponses.test.mjs +11 -0
- package/test/unit-dispatchApiTypesafeSystemone.test.mjs +8 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from 'assert'
|
|
2
2
|
import dispatchAiFallback from '../src/dispatchAiFallback.mjs'
|
|
3
3
|
import createFakeCli from './tools/fakeCliForTest.mjs'
|
|
4
|
+
import fakeServerForApiTest from './tools/fakeServerForApiTest.mjs'
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
//assertKey, 由假CLI回聲之OPENCODE_AUTH_CONTENT解出本次注入之金鑰
|
|
@@ -18,15 +19,20 @@ let getInjectedKey = (stdout) => {
|
|
|
18
19
|
describe('dispatchAiFallback', function() {
|
|
19
20
|
|
|
20
21
|
let fake = null
|
|
22
|
+
let svr = null
|
|
21
23
|
|
|
22
|
-
before(function() {
|
|
24
|
+
before(async function() {
|
|
23
25
|
fake = createFakeCli('fake-fallback')
|
|
26
|
+
svr = await fakeServerForApiTest() //組邊界事件之情境需REST假伺服器(401/截斷/延遲401)
|
|
24
27
|
})
|
|
25
28
|
|
|
26
|
-
after(function() {
|
|
29
|
+
after(async function() {
|
|
27
30
|
if (fake) {
|
|
28
31
|
fake.clean()
|
|
29
32
|
}
|
|
33
|
+
if (svr) {
|
|
34
|
+
await svr.close()
|
|
35
|
+
}
|
|
30
36
|
})
|
|
31
37
|
|
|
32
38
|
it('prompt非有效字串時回傳錯誤結果物件且tried為空', async function() {
|
|
@@ -676,4 +682,241 @@ describe('dispatchAiFallback', function() {
|
|
|
676
682
|
assert.strict.deepEqual(r, rr)
|
|
677
683
|
})
|
|
678
684
|
|
|
685
|
+
it('group-exhausted: 組內每把皆換鑰失敗, 於最後一個next-key之後恰發一次(by為all-keys), tried不增項', async function() {
|
|
686
|
+
let evs = []
|
|
687
|
+
let t = await dispatchAiFallback('abc', {
|
|
688
|
+
providers: [{ id: 'ge-all', kind: 'api-openai-compat', baseURL: svr.url, model: 'echo', keys: ['sk-bad-a', 'sk-bad-b'] }],
|
|
689
|
+
onEvent: (ev) => evs.push(ev),
|
|
690
|
+
})
|
|
691
|
+
let ge = evs.filter((x) => x.type === 'group-exhausted')
|
|
692
|
+
let r = [
|
|
693
|
+
t.ok,
|
|
694
|
+
evs.map((x) => [x.type, x.keyId]),
|
|
695
|
+
ge.map((x) => [x.providerId, x.keyIndex, x.keyId, x.keys, x.attempted, x.by, x.errorTypes]),
|
|
696
|
+
ge[0].error !== '' && ge[0].error === t.tried[1].error, //本組最後一次嘗試之錯誤
|
|
697
|
+
t.tried.map((x) => x.outcome), //組邊界只發事件, 不寫入逐次嘗試歷程
|
|
698
|
+
]
|
|
699
|
+
let rr = [
|
|
700
|
+
false,
|
|
701
|
+
[['try', 'ge-all#0'], ['next-key', 'ge-all#0'], ['try', 'ge-all#1'], ['next-key', 'ge-all#1'], ['group-exhausted', 'ge-all']],
|
|
702
|
+
[['ge-all', null, 'ge-all', 2, 2, 'all-keys', ['http', 'http']]],
|
|
703
|
+
true,
|
|
704
|
+
['next-key', 'next-key'],
|
|
705
|
+
]
|
|
706
|
+
assert.strict.deepEqual(r, rr)
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
it('group-exhausted: 換鑰後遇與金鑰無關之失敗(截斷)整組跳過, by為skip-group', async function() {
|
|
710
|
+
let evs = []
|
|
711
|
+
let t = await dispatchAiFallback('abc', {
|
|
712
|
+
providers: [{ id: 'ge-skip2', kind: 'api-openai-compat', baseURL: svr.url, model: 'trunc-text', keys: ['sk-bad-1', 'sk-good-2'] }],
|
|
713
|
+
onEvent: (ev) => evs.push(ev),
|
|
714
|
+
})
|
|
715
|
+
let ge = evs.filter((x) => x.type === 'group-exhausted')
|
|
716
|
+
let r = [
|
|
717
|
+
t.ok,
|
|
718
|
+
evs.map((x) => [x.type, x.keyId]),
|
|
719
|
+
ge.map((x) => [x.keys, x.attempted, x.by, x.errorTypes]),
|
|
720
|
+
]
|
|
721
|
+
let rr = [
|
|
722
|
+
false,
|
|
723
|
+
[['try', 'ge-skip2#0'], ['next-key', 'ge-skip2#0'], ['try', 'ge-skip2#1'], ['skip-group', 'ge-skip2#1'], ['group-exhausted', 'ge-skip2']],
|
|
724
|
+
[[2, 2, 'skip-group', ['http', 'incomplete']]],
|
|
725
|
+
]
|
|
726
|
+
assert.strict.deepEqual(r, rr)
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
it('group-exhausted: 首把即整組跳過時attempted為1, keys為濾除無效值後之有效金鑰數', async function() {
|
|
730
|
+
let evs = []
|
|
731
|
+
await dispatchAiFallback('abc', {
|
|
732
|
+
providers: [{ id: 'ge-skip1', kind: 'api-openai-compat', baseURL: svr.url, model: 'trunc-text', keys: ['sk-good-1', '', null, 'sk-good-2'] }],
|
|
733
|
+
onEvent: (ev) => evs.push(ev),
|
|
734
|
+
})
|
|
735
|
+
let ge = evs.filter((x) => x.type === 'group-exhausted')
|
|
736
|
+
let r = [
|
|
737
|
+
evs.map((x) => [x.type, x.keyId]),
|
|
738
|
+
ge.map((x) => [x.keys, x.attempted, x.by, x.errorTypes]),
|
|
739
|
+
]
|
|
740
|
+
let rr = [
|
|
741
|
+
[['try', 'ge-skip1#0'], ['skip-group', 'ge-skip1#0'], ['group-exhausted', 'ge-skip1']],
|
|
742
|
+
[[2, 1, 'skip-group', ['incomplete']]],
|
|
743
|
+
]
|
|
744
|
+
assert.strict.deepEqual(r, rr)
|
|
745
|
+
})
|
|
746
|
+
|
|
747
|
+
it('group-exhausted: 組內有任一把成交則不發(含換鑰後成交)', async function() {
|
|
748
|
+
let evs = []
|
|
749
|
+
let t = await dispatchAiFallback('abc', {
|
|
750
|
+
providers: [{ id: 'ge-ok', kind: 'api-openai-compat', baseURL: svr.url, model: 'echo', keys: ['sk-bad-1', 'sk-good-2'] }],
|
|
751
|
+
onEvent: (ev) => evs.push([ev.type, ev.keyId]),
|
|
752
|
+
})
|
|
753
|
+
let r = [t.ok, evs]
|
|
754
|
+
let rr = [true, [['try', 'ge-ok#0'], ['next-key', 'ge-ok#0'], ['try', 'ge-ok#1'], ['ok', 'ge-ok#1']]]
|
|
755
|
+
assert.strict.deepEqual(r, rr)
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
it('group-exhausted: 組內預算用盡或中止(未試完)者不發, 之前已試完之組照發', async function() {
|
|
759
|
+
//預算用盡: 第1把延遲300ms後401, 輪到第2把時剩餘預算不足minAttemptMs → budget-out, 本組未試完
|
|
760
|
+
let evs1 = []
|
|
761
|
+
let t1 = await dispatchAiFallback('abc', {
|
|
762
|
+
providers: [{ id: 'ge-bud', kind: 'api-openai-compat', baseURL: svr.url, model: 'slow-401', keys: ['sk-x-1', 'sk-x-2'] }],
|
|
763
|
+
budgetMs: 1000,
|
|
764
|
+
minAttemptMs: 800,
|
|
765
|
+
onEvent: (ev) => evs1.push([ev.type, ev.keyId]),
|
|
766
|
+
})
|
|
767
|
+
//中止: 輪到第2把時shouldStop轉true → aborted, 本組未試完
|
|
768
|
+
let n2 = 0
|
|
769
|
+
let evs2 = []
|
|
770
|
+
let t2 = await dispatchAiFallback('abc', {
|
|
771
|
+
providers: [{ id: 'ge-abt', kind: 'api-openai-compat', baseURL: svr.url, model: 'echo', keys: ['sk-bad-1', 'sk-bad-2'] }],
|
|
772
|
+
shouldStop: () => n2++ >= 1,
|
|
773
|
+
onEvent: (ev) => evs2.push([ev.type, ev.keyId]),
|
|
774
|
+
})
|
|
775
|
+
//跨組: 第1組已試完(照發), 第2組首把之前中止(不發)
|
|
776
|
+
let n3 = 0
|
|
777
|
+
let evs3 = []
|
|
778
|
+
let t3 = await dispatchAiFallback('abc', {
|
|
779
|
+
providers: [
|
|
780
|
+
{ id: 'ge-abt-a', kind: 'api-openai-compat', baseURL: svr.url, model: 'echo', keys: ['sk-bad-1'] },
|
|
781
|
+
{ id: 'ge-abt-b', kind: 'claude', exe: fake.exe },
|
|
782
|
+
],
|
|
783
|
+
shouldStop: () => n3++ >= 1,
|
|
784
|
+
onEvent: (ev) => evs3.push([ev.type, ev.keyId]),
|
|
785
|
+
})
|
|
786
|
+
//跨組(預算): 第1組已試完(照發), 第2組首把之前剩餘預算不足 → budget-out(不發)
|
|
787
|
+
let evs4 = []
|
|
788
|
+
let t4 = await dispatchAiFallback('abc', {
|
|
789
|
+
providers: [
|
|
790
|
+
{ id: 'ge-bud-a', kind: 'api-openai-compat', baseURL: svr.url, model: 'slow-401', keys: ['sk-q-1'] },
|
|
791
|
+
{ id: 'ge-bud-b', kind: 'claude', exe: fake.exe },
|
|
792
|
+
],
|
|
793
|
+
budgetMs: 1000,
|
|
794
|
+
minAttemptMs: 800,
|
|
795
|
+
onEvent: (ev) => evs4.push([ev.type, ev.keyId]),
|
|
796
|
+
})
|
|
797
|
+
let r = [[t1.errorType, evs1], [t2.errorType, evs2], [t3.errorType, evs3], [t4.errorType, evs4]]
|
|
798
|
+
let rr = [
|
|
799
|
+
['budget', [['try', 'ge-bud#0'], ['next-key', 'ge-bud#0'], ['budget-out', 'ge-bud#1']]],
|
|
800
|
+
['aborted', [['try', 'ge-abt#0'], ['next-key', 'ge-abt#0'], ['aborted', 'ge-abt']]],
|
|
801
|
+
['aborted', [['try', 'ge-abt-a#0'], ['next-key', 'ge-abt-a#0'], ['group-exhausted', 'ge-abt-a'], ['aborted', 'ge-abt-b']]],
|
|
802
|
+
['budget', [['try', 'ge-bud-a#0'], ['next-key', 'ge-bud-a#0'], ['group-exhausted', 'ge-bud-a'], ['budget-out', 'ge-bud-b']]],
|
|
803
|
+
]
|
|
804
|
+
assert.strict.deepEqual(r, rr)
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
it('group-exhausted: 無keys或單把金鑰之條目失敗亦恰發一次(keys為0或1, attempted為1), 回調拋出例外不影響遞補', async function() {
|
|
808
|
+
let evs = []
|
|
809
|
+
let t = await dispatchAiFallback('abc', {
|
|
810
|
+
providers: [
|
|
811
|
+
{ id: 'ge-nokey-a', kind: 'claude', exe: fake.exe, extraArgs: ['--fake-exit=1'] }, //換鑰型失敗(exec)
|
|
812
|
+
{ id: 'ge-nokey-b', kind: 'claude', exe: fake.exe, validate: 'min:100000' }, //與金鑰無關之失敗(validation)
|
|
813
|
+
{ id: 'ge-onekey', kind: 'api-openai-compat', baseURL: svr.url, model: 'echo', keys: ['sk-bad-1'] }, //單把金鑰換鑰型失敗(http)
|
|
814
|
+
{ id: 'ge-badkind', kind: 'no-such-kind', keys: ['k-1', 'k-2'] }, //kind無效, 首把即整組跳過(params)
|
|
815
|
+
{ id: 'ge-nokey-ok', kind: 'claude', exe: fake.exe },
|
|
816
|
+
],
|
|
817
|
+
onEvent: (ev) => {
|
|
818
|
+
evs.push(ev)
|
|
819
|
+
throw new Error('callback error should not break the loop')
|
|
820
|
+
},
|
|
821
|
+
})
|
|
822
|
+
let ge = evs.filter((x) => x.type === 'group-exhausted')
|
|
823
|
+
let r = [
|
|
824
|
+
t.ok,
|
|
825
|
+
t.providerId,
|
|
826
|
+
ge.map((x) => [x.providerId, x.keyIndex, x.keyId, x.keys, x.attempted, x.by, x.errorTypes]),
|
|
827
|
+
]
|
|
828
|
+
let rr = [
|
|
829
|
+
true,
|
|
830
|
+
'ge-nokey-ok',
|
|
831
|
+
[
|
|
832
|
+
['ge-nokey-a', null, 'ge-nokey-a', 0, 1, 'all-keys', ['exec']],
|
|
833
|
+
['ge-nokey-b', null, 'ge-nokey-b', 0, 1, 'skip-group', ['validation']],
|
|
834
|
+
['ge-onekey', null, 'ge-onekey', 1, 1, 'all-keys', ['http']],
|
|
835
|
+
['ge-badkind', null, 'ge-badkind', 2, 1, 'skip-group', ['params']],
|
|
836
|
+
],
|
|
837
|
+
]
|
|
838
|
+
assert.strict.deepEqual(r, rr)
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
it('group-exhausted: 多組鏈中每個試完之組各發一次, 皆位於下一組首個try之前(與cooled並存時殿後)', async function() {
|
|
842
|
+
let stored = { cursors: {}, cooling: {} }
|
|
843
|
+
let store = {
|
|
844
|
+
get: () => stored,
|
|
845
|
+
set: (s) => {
|
|
846
|
+
stored = s
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
let evs = []
|
|
850
|
+
let t = await dispatchAiFallback('abc', {
|
|
851
|
+
providers: [
|
|
852
|
+
{ id: 'ge-m1', kind: 'api-openai-compat', baseURL: svr.url, model: 'echo', keys: ['sk-bad-1', 'sk-bad-2'] },
|
|
853
|
+
{ id: 'ge-m2', kind: 'claude', exe: fake.exe, extraArgs: ['--fake-sleep=9000'], timeoutMs: 500 }, //逾時 → cooled與skip-group
|
|
854
|
+
{ id: 'ge-m3', kind: 'claude', exe: fake.exe },
|
|
855
|
+
],
|
|
856
|
+
store,
|
|
857
|
+
cooldownMs: 300000,
|
|
858
|
+
onEvent: (ev) => evs.push([ev.type, ev.keyId]),
|
|
859
|
+
})
|
|
860
|
+
let r = [t.ok, t.providerId, evs]
|
|
861
|
+
let rr = [
|
|
862
|
+
true,
|
|
863
|
+
'ge-m3',
|
|
864
|
+
[
|
|
865
|
+
['try', 'ge-m1#0'], ['next-key', 'ge-m1#0'], ['try', 'ge-m1#1'], ['next-key', 'ge-m1#1'], ['group-exhausted', 'ge-m1'],
|
|
866
|
+
['try', 'ge-m2'], ['cooled', 'ge-m2'], ['skip-group', 'ge-m2'], ['group-exhausted', 'ge-m2'],
|
|
867
|
+
['try', 'ge-m3'], ['ok', 'ge-m3'],
|
|
868
|
+
],
|
|
869
|
+
]
|
|
870
|
+
assert.strict.deepEqual(r, rr)
|
|
871
|
+
})
|
|
872
|
+
|
|
873
|
+
it('group-exhausted: 並行呼叫共用onEvent與store且跨越一次成交時, 事件數恰等於試完之呼叫數', async function() {
|
|
874
|
+
//以金鑰數與逐把失敗次數重建「整組全敗」時, 並行跨越成交會多計或少計(游標只在成交時推進, 各呼叫起點不同);
|
|
875
|
+
//本事件由各呼叫自身之組迴圈發出, 不受並行交錯影響(對應安裝方報告之多計/少計情境)
|
|
876
|
+
let stored = { cursors: {}, cooling: {} }
|
|
877
|
+
let store = {
|
|
878
|
+
get: () => stored,
|
|
879
|
+
set: (s) => {
|
|
880
|
+
stored = s
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
let count = (evs) => evs.filter((x) => x === 'group-exhausted').length
|
|
884
|
+
//情境一(多計): X與Y同時自游標0起跑, X首把立即成交(游標推進至1), Y兩把皆於成交之後401; 其後Z自新游標1起跑兩把皆敗
|
|
885
|
+
let base1 = { id: 'ge-par1', kind: 'api-openai-compat', baseURL: svr.url }
|
|
886
|
+
let evs1 = []
|
|
887
|
+
let on1 = (ev) => evs1.push(ev.type)
|
|
888
|
+
let [tx, ty] = await Promise.all([
|
|
889
|
+
dispatchAiFallback('abc', { providers: [{ ...base1, model: 'echo', keys: ['sk-x-0', 'sk-x-1'] }], store, onEvent: on1 }),
|
|
890
|
+
dispatchAiFallback('abc', { providers: [{ ...base1, model: 'slow-401', keys: ['sk-y-0', 'sk-y-1'] }], store, onEvent: on1 }),
|
|
891
|
+
])
|
|
892
|
+
let tz = await dispatchAiFallback('abc', { providers: [{ ...base1, model: 'slow-401', keys: ['sk-z-0', 'sk-z-1'] }], store, onEvent: on1 })
|
|
893
|
+
//情境二(少計): W首把立即401(成交之前), V首把成交, W次把延遲300ms後401(成交之後)
|
|
894
|
+
let base2 = { id: 'ge-par2', kind: 'api-openai-compat', baseURL: svr.url }
|
|
895
|
+
let evs2 = []
|
|
896
|
+
let on2 = (ev) => evs2.push(ev.type)
|
|
897
|
+
let [tw, tv] = await Promise.all([
|
|
898
|
+
dispatchAiFallback('abc', { providers: [{ ...base2, model: 'slow-401', keys: ['sk-bad-w-0', 'sk-w-1'] }], store, onEvent: on2 }),
|
|
899
|
+
dispatchAiFallback('abc', { providers: [{ ...base2, model: 'echo', keys: ['sk-v-0', 'sk-v-1'] }], store, onEvent: on2 }),
|
|
900
|
+
])
|
|
901
|
+
let r = [
|
|
902
|
+
[tx.ok, ty.ok, tz.ok],
|
|
903
|
+
ty.tried.map((x) => x.keyId),
|
|
904
|
+
tz.tried.map((x) => x.keyId),
|
|
905
|
+
count(evs1),
|
|
906
|
+
[tw.ok, tv.ok],
|
|
907
|
+
tw.tried.map((x) => x.keyId),
|
|
908
|
+
count(evs2),
|
|
909
|
+
]
|
|
910
|
+
let rr = [
|
|
911
|
+
[true, false, false],
|
|
912
|
+
['ge-par1#0', 'ge-par1#1'],
|
|
913
|
+
['ge-par1#1', 'ge-par1#0'],
|
|
914
|
+
2, //Y與Z各一
|
|
915
|
+
[false, true],
|
|
916
|
+
['ge-par2#0', 'ge-par2#1'],
|
|
917
|
+
1, //W一
|
|
918
|
+
]
|
|
919
|
+
assert.strict.deepEqual(r, rr)
|
|
920
|
+
})
|
|
921
|
+
|
|
679
922
|
})
|
|
@@ -170,10 +170,48 @@ describe('dispatchApiOpenaiCompat', function() {
|
|
|
170
170
|
assert.strict.deepEqual(r, rr)
|
|
171
171
|
})
|
|
172
172
|
|
|
173
|
-
it('呼叫端headers可覆寫Accept-Encoding: 改回允許壓縮時,
|
|
173
|
+
it('呼叫端headers可覆寫Accept-Encoding: 改回允許壓縮時, 遇漏標之壓縮本體即解析失敗且訊息指出本體非JSON與content-encoding=none', async function() {
|
|
174
174
|
let t = await dispatchApiOpenaiCompat('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'br-noheader', headers: { 'Accept-Encoding': 'gzip, deflate' } })
|
|
175
|
-
let r = [t.ok, t.code, t.errorType, t.error]
|
|
176
|
-
let rr = [false, 200, 'invalid-response',
|
|
175
|
+
let r = [t.ok, t.code, t.errorType, t.error.indexOf('INVALID_RESPONSE: body is not JSON (') === 0, t.error.includes('content-encoding=none')]
|
|
176
|
+
let rr = [false, 200, 'invalid-response', true, true]
|
|
177
|
+
assert.strict.deepEqual(r, rr)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('INVALID_RESPONSE分拆: HTTP 200但本體非JSON另報body is not JSON(附原始位元組數、前段hex、content-encoding與content-type), JSON缺content維持原訊息', async function() {
|
|
181
|
+
let t1 = await dispatchApiOpenaiCompat('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'garbage-200' })
|
|
182
|
+
let t2 = await dispatchApiOpenaiCompat('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'not-json' })
|
|
183
|
+
let t3 = await dispatchApiOpenaiCompat('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'no-choices' })
|
|
184
|
+
let r = [
|
|
185
|
+
[t1.ok, t1.code, t1.errorType, t1.error],
|
|
186
|
+
[t2.ok, t2.errorType, t2.error],
|
|
187
|
+
[t3.ok, t3.errorType, t3.error],
|
|
188
|
+
]
|
|
189
|
+
let rr = [
|
|
190
|
+
[false, 200, 'invalid-response', 'INVALID_RESPONSE: body is not JSON (8 bytes, first bytes 8bef0200e4ff1122, content-encoding=none, content-type=application/json)'],
|
|
191
|
+
[false, 'invalid-response', 'INVALID_RESPONSE: body is not JSON (15 bytes, first bytes 706c61696e207465787420626f6479, content-encoding=none, content-type=text/plain)'],
|
|
192
|
+
[false, 'invalid-response', 'INVALID_RESPONSE: missing choices[0].message.content'],
|
|
193
|
+
]
|
|
194
|
+
assert.strict.deepEqual(r, rr)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('本體非JSON與金鑰無關: 遞補鏈只試一把即整組跳過; JSON缺content仍逐把換金鑰', async function() {
|
|
198
|
+
let t1 = await dispatchAiFallback('abc', {
|
|
199
|
+
providers: [
|
|
200
|
+
{ id: 'g-garbage', kind: 'api-openai-compat', baseURL: svr.url, model: 'garbage-200', keys: ['sk-g0', 'sk-g1'] },
|
|
201
|
+
{ id: 'g-ok2', kind: 'api-openai-compat', baseURL: svr.url, model: 'echo', keys: ['sk-o0'] },
|
|
202
|
+
],
|
|
203
|
+
})
|
|
204
|
+
let t2 = await dispatchAiFallback('abc', {
|
|
205
|
+
providers: [{ id: 'g-nochoice', kind: 'api-openai-compat', baseURL: svr.url, model: 'no-choices', keys: ['sk-n0', 'sk-n1'] }],
|
|
206
|
+
})
|
|
207
|
+
let r = [
|
|
208
|
+
[t1.ok, t1.providerId, t1.tried.map((x) => [x.keyId, x.outcome])],
|
|
209
|
+
[t2.ok, t2.tried.map((x) => [x.keyId, x.outcome])],
|
|
210
|
+
]
|
|
211
|
+
let rr = [
|
|
212
|
+
[true, 'g-ok2', [['g-garbage#0', 'skip-group'], ['g-ok2#0', 'ok']]],
|
|
213
|
+
[false, [['g-nochoice#0', 'next-key'], ['g-nochoice#1', 'next-key']]],
|
|
214
|
+
]
|
|
177
215
|
assert.strict.deepEqual(r, rr)
|
|
178
216
|
})
|
|
179
217
|
|
|
@@ -139,6 +139,17 @@ describe('dispatchApiOpenaiResponses', function() {
|
|
|
139
139
|
assert.strict.deepEqual(r, rr)
|
|
140
140
|
})
|
|
141
141
|
|
|
142
|
+
it('INVALID_RESPONSE分拆: HTTP 200但本體非JSON另報body is not JSON(附原始位元組資訊), JSON缺output維持原訊息', async function() {
|
|
143
|
+
let t1 = await dispatchApiOpenaiResponses('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'garbage-200' })
|
|
144
|
+
let t2 = await dispatchApiOpenaiResponses('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'no-output' })
|
|
145
|
+
let r = [[t1.ok, t1.errorType, t1.error], [t2.ok, t2.errorType, t2.error]]
|
|
146
|
+
let rr = [
|
|
147
|
+
[false, 'invalid-response', 'INVALID_RESPONSE: body is not JSON (8 bytes, first bytes 8bef0200e4ff1122, content-encoding=none, content-type=application/json)'],
|
|
148
|
+
[false, 'invalid-response', 'INVALID_RESPONSE: missing output array'],
|
|
149
|
+
]
|
|
150
|
+
assert.strict.deepEqual(r, rr)
|
|
151
|
+
})
|
|
152
|
+
|
|
142
153
|
it('validate拋錯視同驗證失敗, Promise照常resolve不reject', async function() {
|
|
143
154
|
let boom = () => {
|
|
144
155
|
throw new Error('boom-validate')
|
|
@@ -94,11 +94,18 @@ describe('dispatchApiTypesafeSystemone', function() {
|
|
|
94
94
|
let rr = [
|
|
95
95
|
[false, 'invalid-response', 'INVALID_RESPONSE: missing answers for [urgent]', null, '', 'jev-1.13.0', { input_tokens: 7, output_tokens: 3 }],
|
|
96
96
|
[false, 'invalid-response', 'INVALID_RESPONSE: missing answers object'],
|
|
97
|
-
[false, 'invalid-response', 'INVALID_RESPONSE:
|
|
97
|
+
[false, 'invalid-response', 'INVALID_RESPONSE: body is not JSON (15 bytes, first bytes 706c61696e207465787420626f6479, content-encoding=none, content-type=text/plain)'],
|
|
98
98
|
]
|
|
99
99
|
assert.strict.deepEqual(r, rr)
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
+
it('HTTP 200但本體非JSON另報body is not JSON(附原始位元組資訊, 同dispatchApiOpenaiCompat)', async function() {
|
|
103
|
+
let t = await dispatchApiTypesafeSystemone('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'garbage-200', questions: Q })
|
|
104
|
+
let r = [t.ok, t.errorType, t.error]
|
|
105
|
+
let rr = [false, 'invalid-response', 'INVALID_RESPONSE: body is not JSON (8 bytes, first bytes 8bef0200e4ff1122, content-encoding=none, content-type=application/json)']
|
|
106
|
+
assert.strict.deepEqual(r, rr)
|
|
107
|
+
})
|
|
108
|
+
|
|
102
109
|
it('壞金鑰401、未知model 400、題型不合規422皆為http且4xx不重試, 原始本體置stderr', async function() {
|
|
103
110
|
let t1 = await dispatchApiTypesafeSystemone('abc', { baseURL: svr.url, key: 'sk-bad', model: 'echo', questions: Q, maxRetries: 3 })
|
|
104
111
|
let t2 = await dispatchApiTypesafeSystemone('abc', { baseURL: svr.url, key: 'sk-good-1', model: 'jev-nope', questions: Q, maxRetries: 3 })
|