dsh-plugin-litellm-bridge 2.0.7 → 2.0.9
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.
|
@@ -582,6 +582,29 @@ window.__ModuleLoader__.load({
|
|
|
582
582
|
}
|
|
583
583
|
}, [draft, pid]);
|
|
584
584
|
|
|
585
|
+
// 模型池按供应商分组,供下拉与"添加"列表使用
|
|
586
|
+
//
|
|
587
|
+
// 【React #310 血案】这个 useMemo 原来写在下面两个提前 return **之后**:
|
|
588
|
+
// if (!draft) return ...; ← 首次渲染 draft=null,从这里返回
|
|
589
|
+
// if (!prof) return ...;
|
|
590
|
+
// const byProvider = useMemo(...) ← 首次渲染根本执行不到
|
|
591
|
+
// 第二次渲染(routes/get 返回后 draft 有值)才执行到这一行,
|
|
592
|
+
// hooks 数量从 N 变成 N+1 → React 抛
|
|
593
|
+
// "Rendered more hooks than during the previous render."(#310)
|
|
594
|
+
// 表现:点「路由配置」→ 整页空白。
|
|
595
|
+
//
|
|
596
|
+
// 规则:**所有 hooks 必须写在任何条件 return 之前**,保证每次渲染
|
|
597
|
+
// 执行完全一样多的 hooks。所以把它移到这里。
|
|
598
|
+
const byProvider = useMemo(() => {
|
|
599
|
+
const m = new Map();
|
|
600
|
+
for (const x of pool.models || []) {
|
|
601
|
+
const key = x.provider || '未知供应商';
|
|
602
|
+
if (!m.has(key)) m.set(key, []);
|
|
603
|
+
m.get(key).push(x);
|
|
604
|
+
}
|
|
605
|
+
return [...m.entries()];
|
|
606
|
+
}, [pool.models]);
|
|
607
|
+
|
|
585
608
|
// 加载失败时给出可读原因 + 重试,而不是无限「加载中…」
|
|
586
609
|
if (!draft) {
|
|
587
610
|
return h('div', null,
|
|
@@ -614,24 +637,24 @@ window.__ModuleLoader__.load({
|
|
|
614
637
|
return next;
|
|
615
638
|
});
|
|
616
639
|
|
|
617
|
-
// ----
|
|
640
|
+
// ---- 拖拽排序:整条链(首选 + 备选)的先后顺序即降级优先级 ----
|
|
618
641
|
const onDragStart = (at, idx) => { dragRef.current = { at, idx }; };
|
|
619
|
-
|
|
642
|
+
/**
|
|
643
|
+
* 拖拽落点:把 src 位置的模型插到 target 位置。
|
|
644
|
+
* 操作对象是**整条链**(primary 是第 0 位),
|
|
645
|
+
* 所以拖到第 1 位就等于「换首选」,不需要额外按钮。
|
|
646
|
+
*/
|
|
647
|
+
const onDropChain = (at, idx) => {
|
|
620
648
|
const src = dragRef.current;
|
|
649
|
+
dragRef.current = null;
|
|
621
650
|
if (!src || src.at !== at || src.idx === idx) return;
|
|
622
|
-
const
|
|
651
|
+
const cfg = prof?.agents?.[at];
|
|
652
|
+
if (!cfg) return;
|
|
653
|
+
const list = [cfg.primary, ...(cfg.fallbacks || [])].filter(Boolean);
|
|
654
|
+
if (src.idx < 0 || src.idx >= list.length) return;
|
|
623
655
|
const [moved] = list.splice(src.idx, 1);
|
|
624
656
|
list.splice(idx, 0, moved);
|
|
625
|
-
setAgent(at, { fallbacks: list });
|
|
626
|
-
dragRef.current = null;
|
|
627
|
-
};
|
|
628
|
-
|
|
629
|
-
/** 把某个模型从「备选」提升为「首选」,原首选下沉为第一备选。 */
|
|
630
|
-
const promote = (at, idx) => {
|
|
631
|
-
const cfg = prof.agents[at];
|
|
632
|
-
const target = cfg.fallbacks[idx];
|
|
633
|
-
const rest = cfg.fallbacks.filter((_, i) => i !== idx);
|
|
634
|
-
setAgent(at, { primary: target, fallbacks: cfg.primary ? [cfg.primary, ...rest] : rest });
|
|
657
|
+
setAgent(at, { primary: list[0] || '', fallbacks: list.slice(1) });
|
|
635
658
|
};
|
|
636
659
|
|
|
637
660
|
const saveSection = async (at) => {
|
|
@@ -700,20 +723,27 @@ window.__ModuleLoader__.load({
|
|
|
700
723
|
setBusy(false);
|
|
701
724
|
};
|
|
702
725
|
|
|
703
|
-
// 模型池按供应商分组,供下拉与"添加"列表使用
|
|
704
|
-
const byProvider = useMemo(() => {
|
|
705
|
-
const m = new Map();
|
|
706
|
-
for (const x of pool.models || []) {
|
|
707
|
-
const key = x.provider || '未知供应商';
|
|
708
|
-
if (!m.has(key)) m.set(key, []);
|
|
709
|
-
m.get(key).push(x);
|
|
710
|
-
}
|
|
711
|
-
return [...m.entries()];
|
|
712
|
-
}, [pool.models]);
|
|
713
|
-
|
|
714
726
|
const allIds = (pool.models || []).map((x) => x.id);
|
|
715
727
|
|
|
716
728
|
return h('div', null,
|
|
729
|
+
// ---- 使用引导:第一次进来的人 3 秒看懂这页在做什么 ----
|
|
730
|
+
h('div', {
|
|
731
|
+
style: {
|
|
732
|
+
padding: '10px 12px', marginBottom: 18, borderRadius: 6,
|
|
733
|
+
background: tint(T.brand, 6), border: `1px solid ${tint(T.brand, 25)}`,
|
|
734
|
+
fontSize: 12, lineHeight: 1.7, color: T.labelPrimary,
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
h('div', { style: { fontWeight: 600, marginBottom: 4 } }, '这页在做什么'),
|
|
738
|
+
h('div', null,
|
|
739
|
+
'给三类 Agent 各配一条「降级链」:先试第 1 个模型,失败就换第 2 个,再失败换第 3 个。',
|
|
740
|
+
),
|
|
741
|
+
h('div', { style: { marginTop: 4 } },
|
|
742
|
+
'配好后点最下方「保存并下发到网关」,网关就会按这个顺序执行重试。'),
|
|
743
|
+
h('div', { style: { marginTop: 6, opacity: 0.75 } },
|
|
744
|
+
'提示:不用每条链都配。只配 root 也能用;coder / research 留空表示沿用 DSH 原来的行为。'),
|
|
745
|
+
),
|
|
746
|
+
|
|
717
747
|
// ---- Profile 分组:选择 / 新建 / 删除 ----
|
|
718
748
|
h('div', { style: S.h2Row },
|
|
719
749
|
h('h2', { style: S.h2 }, 'Profile 分组'),
|
|
@@ -735,6 +765,9 @@ window.__ModuleLoader__.load({
|
|
|
735
765
|
}, '删除当前'),
|
|
736
766
|
),
|
|
737
767
|
),
|
|
768
|
+
h('div', { style: { ...S.hint, marginTop: -6, marginBottom: 14 } },
|
|
769
|
+
'一个 profile = 一套降级方案。可以配多套(例如「稳定优先」「省钱优先」),' +
|
|
770
|
+
'再到「当前会话」里选用哪一套。'),
|
|
738
771
|
|
|
739
772
|
showNew && h('div', { style: { ...S.row, marginBottom: 14 } },
|
|
740
773
|
h('input', {
|
|
@@ -762,18 +795,90 @@ window.__ModuleLoader__.load({
|
|
|
762
795
|
// 直接读 cfg.primary 会抛 TypeError,进而让整页崩掉。
|
|
763
796
|
const cfg = prof?.agents?.[at]
|
|
764
797
|
|| { label: at, primary: '', timeoutMs: 600000, fallbacks: [] };
|
|
798
|
+
|
|
799
|
+
const chain = [cfg.primary, ...(cfg.fallbacks || [])].filter(Boolean);
|
|
765
800
|
const isSub = at !== 'root';
|
|
766
|
-
const groupLabel = at === 'root' ? 'root · 主代理降级链'
|
|
767
|
-
: at === 'coder' ? 'coder · 子代理降级链(Coder)'
|
|
768
|
-
: 'research · 子代理降级链(Research)';
|
|
769
|
-
const chainLen = [cfg.primary, ...(cfg.fallbacks || [])].filter(Boolean).length;
|
|
770
801
|
|
|
771
|
-
|
|
802
|
+
// 每个角色的说明:用「人话」讲清楚它管什么
|
|
803
|
+
const meta = at === 'root'
|
|
804
|
+
? {
|
|
805
|
+
title: 'root · 主代理',
|
|
806
|
+
desc: '你自己直接对话的那个 Agent。它出问题时按下面的顺序换模型重试。',
|
|
807
|
+
hint: '通常放最稳、上下文最大的模型。',
|
|
808
|
+
}
|
|
809
|
+
: at === 'coder'
|
|
810
|
+
? {
|
|
811
|
+
title: 'coder · 子代理(写代码)',
|
|
812
|
+
desc: 'Agent 里 spawn 出来的「写代码」子任务用它,和 root 完全独立。',
|
|
813
|
+
hint: '可放长输出、便宜的模型。',
|
|
814
|
+
}
|
|
815
|
+
: {
|
|
816
|
+
title: 'research · 子代理(调研)',
|
|
817
|
+
desc: 'Agent 里 spawn 出来的「调研/检索/分析」子任务用它。',
|
|
818
|
+
hint: '可放推理强或便宜的模型。',
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
/** 修改整条链(统一入口,避免 primary/fallbacks 两个来源打架) */
|
|
822
|
+
const setChain = (nextList) => {
|
|
823
|
+
const list = nextList.filter(Boolean);
|
|
824
|
+
setAgent(at, { primary: list[0] || '', fallbacks: list.slice(1) });
|
|
825
|
+
};
|
|
826
|
+
const moveUp = (i) => {
|
|
827
|
+
if (i <= 0) return;
|
|
828
|
+
const l = [...chain];
|
|
829
|
+
[l[i - 1], l[i]] = [l[i], l[i - 1]];
|
|
830
|
+
setChain(l);
|
|
831
|
+
};
|
|
832
|
+
const removeAt = (i) => setChain(chain.filter((_, j) => j !== i));
|
|
833
|
+
const addModel = (id) => {
|
|
834
|
+
if (!id || chain.includes(id)) return;
|
|
835
|
+
setChain([...chain, id]);
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
/** 一个可拖拽的链节点 */
|
|
839
|
+
const row = (name, i) => h('div', {
|
|
840
|
+
key: `${at}-${name}-${i}`,
|
|
841
|
+
draggable: true,
|
|
842
|
+
onDragStart: () => onDragStart(at, i),
|
|
843
|
+
onDragOver: (e) => e.preventDefault(),
|
|
844
|
+
onDrop: () => onDropChain(at, i),
|
|
845
|
+
style: {
|
|
846
|
+
display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px',
|
|
847
|
+
marginBottom: 4, borderRadius: 6, cursor: 'grab',
|
|
848
|
+
background: i === 0 ? tint(T.success, 7) : T.bgLayer2,
|
|
849
|
+
border: `1px solid ${i === 0 ? tint(T.success, 30) : T.borderL1}`,
|
|
850
|
+
},
|
|
851
|
+
},
|
|
852
|
+
h('span', { style: { opacity: 0.4, ...S.mono } }, '⠿'),
|
|
853
|
+
h('span', {
|
|
854
|
+
style: {
|
|
855
|
+
...S.mono, fontSize: 11, width: 20, textAlign: 'center',
|
|
856
|
+
flex: '0 0 20px', fontWeight: 600,
|
|
857
|
+
color: i === 0 ? T.success : T.labelSecondary,
|
|
858
|
+
},
|
|
859
|
+
}, String(i + 1)),
|
|
860
|
+
h('span', { style: { flex: '1 1 auto', ...S.mono, wordBreak: 'break-all' } }, safeText(name)),
|
|
861
|
+
i === 0
|
|
862
|
+
? h('span', { style: S.badge(T.success, tint(T.success, 16)) }, '先用这个')
|
|
863
|
+
: h('span', { style: S.badge() }, i === 1 ? '失败后换它' : `${i + 1} 顺位`),
|
|
864
|
+
i > 0 && h('button', {
|
|
865
|
+
style: { ...S.btnSm, height: 22, fontSize: 11 },
|
|
866
|
+
onClick: () => moveUp(i),
|
|
867
|
+
title: '往前挪一位(往前 = 更优先)',
|
|
868
|
+
}, '↑'),
|
|
869
|
+
h('button', {
|
|
870
|
+
style: { ...S.btnSm, height: 22, fontSize: 11, color: T.error, borderColor: tint(T.error, 35) },
|
|
871
|
+
onClick: () => removeAt(i),
|
|
872
|
+
}, '删'),
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
return h('div', { key: at, style: { marginBottom: 22 } },
|
|
876
|
+
// 标题行
|
|
772
877
|
h('div', { style: S.h2Row },
|
|
773
|
-
h('h2', { style: S.h2 }, safeText(
|
|
878
|
+
h('h2', { style: S.h2 }, safeText(meta.title)),
|
|
774
879
|
isSub && h('span', { style: S.badge() }, '仅子 Agent 使用'),
|
|
775
880
|
h('span', { style: { ...S.mono, fontSize: 11, color: T.labelSecondary } },
|
|
776
|
-
|
|
881
|
+
chain.length ? `${chain.length} 个模型` : '还没配'),
|
|
777
882
|
h('div', { style: S.sectionActions },
|
|
778
883
|
h('button', {
|
|
779
884
|
style: S.btnSm,
|
|
@@ -782,97 +887,84 @@ window.__ModuleLoader__.load({
|
|
|
782
887
|
next[pid].agents[at] = JSON.parse(JSON.stringify(state.routes[pid].agents[at]));
|
|
783
888
|
return next;
|
|
784
889
|
}),
|
|
785
|
-
}, '
|
|
890
|
+
}, '撤销修改'),
|
|
786
891
|
h('button', {
|
|
787
892
|
style: { ...S.btn('primary'), height: 26, padding: '0 10px', fontSize: 12 },
|
|
788
893
|
disabled: busy,
|
|
789
894
|
onClick: () => saveSection(at),
|
|
790
|
-
}, '
|
|
895
|
+
}, '保存这条链'),
|
|
791
896
|
),
|
|
792
897
|
),
|
|
793
898
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
899
|
+
// 说明(讲清楚这一链管什么)
|
|
900
|
+
h('div', { style: { ...S.hint, marginTop: -4, marginBottom: 10 } },
|
|
901
|
+
safeText(meta.desc),
|
|
902
|
+
h('span', { style: { opacity: 0.7 } }, ` 建议:${safeText(meta.hint)}`)),
|
|
903
|
+
|
|
904
|
+
// 完整降级序列(首选 + 备选合在一起看)
|
|
905
|
+
chain.length === 0
|
|
906
|
+
? h('div', {
|
|
907
|
+
style: {
|
|
908
|
+
padding: '14px 12px', marginBottom: 8, borderRadius: 6,
|
|
909
|
+
background: T.bgLayer1, border: `1px dashed ${T.borderL2}`,
|
|
910
|
+
fontSize: 12, color: T.labelSecondary, textAlign: 'center',
|
|
911
|
+
},
|
|
912
|
+
}, '还没有选模型。从下面的下拉框加第一个 —— 它就是这个链的首选。')
|
|
913
|
+
: h('div', { style: { marginBottom: 8 } },
|
|
914
|
+
h('div', { style: { ...S.label } }, '降级顺序(从上到下 = 先试这个,不行再试下一个)'),
|
|
915
|
+
chain.map((name, i) => row(name, i)),
|
|
807
916
|
),
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
}
|
|
917
|
+
|
|
918
|
+
// 追加模型
|
|
919
|
+
h('div', { style: { ...S.row, marginBottom: 10 } },
|
|
920
|
+
h('select', {
|
|
921
|
+
style: { ...S.inputAuto, flex: '1 1 300px' },
|
|
922
|
+
value: '',
|
|
923
|
+
onChange: (e) => { addModel(e.target.value); e.target.value = ''; },
|
|
924
|
+
},
|
|
925
|
+
h('option', { value: '' },
|
|
926
|
+
chain.length ? '+ 再加一个备选模型' : '+ 选择这个链的第一个模型'),
|
|
927
|
+
byProvider.map(([prov, provRows]) => h('optgroup', { key: prov, label: prov },
|
|
928
|
+
provRows.filter((m) => !chain.includes(m.id)).map((m) =>
|
|
929
|
+
h('option', { key: `${prov}:${m.id}`, value: m.id }, m.id)),
|
|
930
|
+
)),
|
|
815
931
|
),
|
|
816
932
|
),
|
|
817
933
|
|
|
818
|
-
//
|
|
819
|
-
h('div', { style: S.
|
|
820
|
-
h('
|
|
821
|
-
(
|
|
822
|
-
|
|
823
|
-
:
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
marginBottom: 3, borderRadius: 5, cursor: 'grab',
|
|
831
|
-
background: T.bgLayer2, border: `1px solid ${T.borderL1}`,
|
|
832
|
-
},
|
|
833
|
-
},
|
|
834
|
-
h('span', { style: { opacity: 0.45, ...S.mono } }, '⠿'),
|
|
835
|
-
h('span', { style: S.badge() }, `第 ${i + 2} 顺位`),
|
|
836
|
-
h('span', { style: { flex: 1, ...S.mono, wordBreak: 'break-all' } }, f),
|
|
837
|
-
h('button', { style: S.btnSm, onClick: () => promote(at, i), title: '提升为首选' }, '↑ 首选'),
|
|
838
|
-
h('button', {
|
|
839
|
-
style: S.btnSm,
|
|
840
|
-
onClick: () => setAgent(at, { fallbacks: cfg.fallbacks.filter((_, j) => j !== i) }),
|
|
841
|
-
}, '移除'),
|
|
842
|
-
)),
|
|
843
|
-
|
|
844
|
-
// 追加备选:下拉选 DSH 模型池里的模型
|
|
845
|
-
h('div', { style: { ...S.row, marginTop: 7 } },
|
|
846
|
-
h('select', {
|
|
847
|
-
style: { ...S.inputAuto, flex: '1 1 260px' },
|
|
848
|
-
value: '',
|
|
849
|
-
onChange: (e) => {
|
|
850
|
-
const v = e.target.value;
|
|
851
|
-
if (!v) return;
|
|
852
|
-
const cur = cfg.fallbacks || [];
|
|
853
|
-
if (v === cfg.primary || cur.includes(v)) return;
|
|
854
|
-
setAgent(at, { fallbacks: [...cur, v] });
|
|
855
|
-
e.target.value = '';
|
|
856
|
-
},
|
|
857
|
-
},
|
|
858
|
-
h('option', { value: '' }, '+ 添加备选模型(从 DSH 模型池选择)'),
|
|
859
|
-
byProvider.map(([prov, rows]) => h('optgroup', { key: prov, label: prov },
|
|
860
|
-
rows.filter((m) => m.id !== cfg.primary).map((m) =>
|
|
861
|
-
h('option', { key: `${prov}:${m.id}`, value: m.id }, m.id)),
|
|
862
|
-
)),
|
|
863
|
-
),
|
|
864
|
-
),
|
|
865
|
-
allIds.length === 0 && h('div', { style: { ...S.hint, color: T.warn } },
|
|
866
|
-
'模型池为空:请先到【设置 → 模型】里配置供应商,并用【获取可用模型】拉取模型目录。'),
|
|
934
|
+
// 超时(放在最后,因为它多数情况不用改)
|
|
935
|
+
h('div', { style: { ...S.row } },
|
|
936
|
+
h('span', { style: { ...S.label, marginBottom: 0 } }, '单次请求超时'),
|
|
937
|
+
h('input', {
|
|
938
|
+
style: { ...S.inputAuto, width: 120 },
|
|
939
|
+
type: 'number',
|
|
940
|
+
value: cfg.timeoutMs,
|
|
941
|
+
onChange: (e) => setAgent(at, { timeoutMs: Number(e.target.value) }),
|
|
942
|
+
}),
|
|
943
|
+
h('span', { style: { ...S.mono, fontSize: 11, color: T.labelSecondary } }, '毫秒'),
|
|
944
|
+
h('span', { style: { ...S.mono, fontSize: 11, color: T.labelSecondary } },
|
|
945
|
+
`(约 ${Math.round((cfg.timeoutMs || 0) / 60000)} 分钟)`),
|
|
867
946
|
),
|
|
947
|
+
|
|
948
|
+
allIds.length === 0 && h('div', { style: { ...S.hint, color: T.warn } },
|
|
949
|
+
'模型池为空:请先到【设置 → 模型】里配置供应商,并用【获取可用模型】拉取模型目录。'),
|
|
868
950
|
);
|
|
869
951
|
}),
|
|
870
952
|
|
|
871
953
|
// ---- 下发区 ----
|
|
872
|
-
h('div', {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
954
|
+
h('div', {
|
|
955
|
+
style: {
|
|
956
|
+
marginTop: 22, paddingTop: 16, borderTop: `1px solid ${T.borderL1}`,
|
|
957
|
+
},
|
|
958
|
+
},
|
|
959
|
+
h('div', { style: { ...S.label } }, '最后一步'),
|
|
960
|
+
h('div', { style: { ...S.hint, marginBottom: 10 } },
|
|
961
|
+
'「保存并下发」会把上面三条链写成网关配置并重启网关,让它按这个顺序执行。' +
|
|
962
|
+
'只保存不下发的话,网关不会生效。'),
|
|
963
|
+
h('div', { style: { ...S.row } },
|
|
964
|
+
h('button', { style: S.btn('primary'), onClick: applyChains, disabled: busy },
|
|
965
|
+
busy ? '处理中…' : '保存并下发到网关'),
|
|
966
|
+
h('button', { style: S.btn(), onClick: preview, disabled: busy }, '先看看生成的配置'),
|
|
967
|
+
),
|
|
876
968
|
),
|
|
877
969
|
|
|
878
970
|
msg && h('div', {
|
|
@@ -93,6 +93,34 @@ export function statePath() {
|
|
|
93
93
|
return join(stateDir(), 'state.json');
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* 旧版遗留的「假模型名」。
|
|
98
|
+
*
|
|
99
|
+
* 插件 1.x 时代网关自己维护 model_list,写死过一批别名
|
|
100
|
+
* (root-primary / coder-fallback-1 之类)。架构修正后模型池改由
|
|
101
|
+
* DSH【设置-模型】提供,这些名字**在真实模型池里一个都不存在**。
|
|
102
|
+
*
|
|
103
|
+
* 用户机器上的 state.json 会带着它们,表现就是:
|
|
104
|
+
* 首选模型下拉显示「— 请选择 —」,但下面 fallback 里挂着一串
|
|
105
|
+
* 永远连不上的假名字 —— 这就是「看不懂怎么配」的直接来源。
|
|
106
|
+
*
|
|
107
|
+
* 这里做一个自动迁移:加载状态时把它们清掉,让界面回到干净骨架。
|
|
108
|
+
*/
|
|
109
|
+
const LEGACY_MODEL_RE = /^(root|coder|research)-(primary|fallback-\d+|profile-\w+)$/;
|
|
110
|
+
|
|
111
|
+
function isLegacyModelName(name) {
|
|
112
|
+
return typeof name === 'string' && LEGACY_MODEL_RE.test(name.trim());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 清理一条链里所有旧版假模型名,返回清理后的链与是否发生变更。 */
|
|
116
|
+
function stripLegacyModels(chain) {
|
|
117
|
+
const before = JSON.stringify(chain);
|
|
118
|
+
const primary = isLegacyModelName(chain.primary) ? '' : chain.primary;
|
|
119
|
+
const fallbacks = (chain.fallbacks || []).filter((f) => !isLegacyModelName(f));
|
|
120
|
+
const after = JSON.stringify({ ...chain, primary, fallbacks });
|
|
121
|
+
return { chain: { ...chain, primary, fallbacks }, changed: before !== after };
|
|
122
|
+
}
|
|
123
|
+
|
|
96
124
|
/** 归一化一条 agent 链(容忍旧版本 / 残缺字段)。 */
|
|
97
125
|
function mergeAgentChain(src, base) {
|
|
98
126
|
const out = { ...base };
|
|
@@ -128,6 +156,9 @@ function mergeState(base, loaded) {
|
|
|
128
156
|
};
|
|
129
157
|
for (const at of AGENT_TYPES) {
|
|
130
158
|
out.agents[at] = mergeAgentChain(prof?.agents?.[at], skeleton.agents[at]);
|
|
159
|
+
// 自动清理旧版遗留的假模型名(root-primary 之类)
|
|
160
|
+
const { chain } = stripLegacyModels(out.agents[at]);
|
|
161
|
+
out.agents[at] = chain;
|
|
131
162
|
}
|
|
132
163
|
merged.routes[pid] = out;
|
|
133
164
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-litellm-bridge",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.9",
|
|
4
4
|
"description": "LiteLLM gateway bridge for DSH: assemble root / coder / research fallback chains from the DSH model pool and hand the order to a LiteLLM gateway that executes retries and fallbacks. The plugin performs no routing itself.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|