dsh-plugin-litellm-bridge 0.0.0-stage → 2.0.2

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.
@@ -0,0 +1,27 @@
1
+ # litellm-bridge 的 profile-bundle 补丁:一行 insert,把本插件挂进 profile 的组合树。
2
+ #
3
+ # 说明(照 dsh-llm-fallbacks 的约定):
4
+ # - 本行只声明中性默认值;用户级 profile cordis.patch.yml 里的覆盖优先级更高。
5
+ # - 补丁会「整块替换」目标行的 config 而不是合并,所以用户侧覆盖要写全字段。
6
+ # - 本插件与 dsh-llm-fallbacks「不应同时启用」:两套降级逻辑会互相冲突。
7
+ # 安装前请先在插件管理器里禁用 dsh-llm-fallbacks。
8
+
9
+ - insert:
10
+ - id: litellm-bridge
11
+ name: 'dsh-plugin-litellm-bridge'
12
+ config:
13
+ # LiteLLM 网关地址(UI「网关设置」Tab 可改)
14
+ gatewayUrl: 'http://127.0.0.1:4000'
15
+ # 请求超时(毫秒)
16
+ timeoutMs: 30000
17
+ # 日志面板默认保留条数(防止臃肿)
18
+ logLimit: 50
19
+ # 探测到可用热重载端点后写在这里;留空表示每次自动探测
20
+ reloadEndpoint: ''
21
+ # ---- 子代理路由覆盖 ----
22
+ # 会话选中这些供应商时,子代理强制改道网关(主 Agent 保持原样)
23
+ nativeProviders:
24
+ - trae
25
+ - workbuddy
26
+ # 子代理被改道到的 provider 名(需与 DSH 里指向本网关的 provider 路由一致)
27
+ gatewayProvider: 'litellm-gateway'
@@ -0,0 +1,12 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
2
+ <!-- 桥接:左右两个节点经中间网关转发 -->
3
+ <circle cx="3.5" cy="12" r="2"/>
4
+ <circle cx="20.5" cy="6" r="1.7"/>
5
+ <circle cx="20.5" cy="12" r="1.7"/>
6
+ <circle cx="20.5" cy="18" r="1.7"/>
7
+ <path d="M5.5 12h4.2"/>
8
+ <path d="M15.8 12h2.7"/>
9
+ <path d="M9.7 12c1.6-.9 1.6-5.1 3.3-6h5.8"/>
10
+ <path d="M9.7 12c1.6.9 1.6 5.1 3.3 6h5.8"/>
11
+ <rect x="9.7" y="9.6" width="6.1" height="4.8" rx="1.1"/>
12
+ </svg>
@@ -0,0 +1,193 @@
1
+ /**
2
+ * profile 降级链 → LiteLLM config 片段
3
+ * ============================================================================
4
+ *
5
+ * 【架构修正需求 3】路由配置 Tab 保存的 profile × (root / coder / research)
6
+ * 三条降级序列,必须**下发给 LiteLLM 网关**;网关只按顺序执行重试 / 降级,
7
+ * 插件本身不含路由逻辑。
8
+ *
9
+ * 【这一版最关键的修正 · 上一版是错的】
10
+ * 上一版把上游写成 `os.environ/LITELLM_UPSTREAM_<模型名大写>` —— 这些
11
+ * 环境变量**没有任何人设置**,网关启动后每条链都是坏的(表面成功、实际不通)。
12
+ * 正确做法:模型来自 DSH【设置-模型】,那里的供应商已配好 api_base + api_key。
13
+ * 所以按**供应商**解析到真实的 api_base 与凭证环境变量名;拿不到就明确报警,
14
+ * 绝不生成一条看起来正常、实际调不通的配置。
15
+ *
16
+ * 【必须纯 ASCII】LiteLLM 用平台默认编码读 config.yaml;中文 Windows 是 GBK,
17
+ * 非 ASCII 注释会在启动 / 重载时抛 UnicodeDecodeError(实测踩过)。
18
+ */
19
+
20
+ /** 默认按 OpenAI 兼容协议发请求。 */
21
+ const OPENAI_COMPAT = 'openai';
22
+
23
+ /**
24
+ * 供应商协议 → LiteLLM model 前缀。
25
+ * LiteLLM 认这些前缀,用对协议发请求,否则会 404 / 400。
26
+ */
27
+ export function protocolPrefix(provider) {
28
+ const k = String(provider?.kind || provider?.id || provider?.name || '').toLowerCase();
29
+ if (k.includes('anthropic') || k.includes('claude')) return 'anthropic';
30
+ if (k.includes('gemini') || k.includes('google')) return 'gemini';
31
+ return OPENAI_COMPAT;
32
+ }
33
+
34
+ /**
35
+ * 供应商的凭证环境变量名。
36
+ *
37
+ * DSH 自己管凭证;网关是独立进程,只能通过环境变量拿到同一个 key。
38
+ * 优先用供应商声明的 nativeAuth.env / baseURLEnv,没有就退回约定名。
39
+ * 名字本身不含密钥,写进 YAML 是安全的(os.environ/XXX 只引用不展开)。
40
+ */
41
+ export function keyEnvName(provider) {
42
+ const env = provider?.authEnv?.[0] || provider?.baseUrlEnv?.[0];
43
+ if (env) return env;
44
+ return `${String(provider?.id || 'provider').toUpperCase().replace(/[^A-Z0-9]/g, '_')}_API_KEY`;
45
+ }
46
+
47
+ /**
48
+ * 组装 config.yaml。
49
+ *
50
+ * @param {object} state ConfigStore 状态(含 routes)
51
+ * @param {object} [pool] collectModelPool() 结果(供应商 baseURL / 凭证 env)
52
+ * @returns {{yaml:string, warnings:string[], chains:Array, upstreams:Array}}
53
+ */
54
+ export function assembleChainConfig(state, pool = null) {
55
+ const routes = state?.routes || {};
56
+ const providerMap = new Map((pool?.providers || []).map((p) => [p.id, p]));
57
+ /** model id → 归属供应商(池里第一个出现的) */
58
+ const providerOfModel = new Map();
59
+ for (const m of pool?.models || []) {
60
+ if (!providerOfModel.has(m.id)) providerOfModel.set(m.id, m.provider);
61
+ }
62
+
63
+ const warnings = [];
64
+ /** 真实上游:model id → { name, provider, baseUrl, keyEnv, timeoutSec } */
65
+ const upstreams = new Map();
66
+ /** 链:{ pid, at, head, chain, timeoutMs } */
67
+ const chains = [];
68
+
69
+ // ---- 1. 收集所有链与上游 ------------------------------------------------
70
+ for (const pid of Object.keys(routes)) {
71
+ const agents = routes[pid]?.agents || {};
72
+ for (const at of Object.keys(agents)) {
73
+ const cfg = agents[at];
74
+ if (!cfg?.primary) continue;
75
+ const chain = [cfg.primary, ...(cfg.fallbacks || [])].filter(Boolean);
76
+ if (!chain.length) continue;
77
+
78
+ chains.push({
79
+ pid,
80
+ at,
81
+ head: `${at}-${pid}-primary`,
82
+ chain,
83
+ timeoutMs: cfg.timeoutMs || 600000,
84
+ });
85
+
86
+ for (const modelId of chain) {
87
+ if (upstreams.has(modelId)) continue;
88
+ const providerId = providerOfModel.get(modelId) || '';
89
+ const prov = providerMap.get(providerId);
90
+ if (!providerId) {
91
+ warnings.push(
92
+ `模型 ${modelId} 不在 DSH 模型池里(可能已被删除或改了名)。` +
93
+ '请到【设置-模型】确认,或在「路由配置」里重新选择。',
94
+ );
95
+ continue;
96
+ }
97
+ if (!prov?.baseUrl) {
98
+ warnings.push(
99
+ `供应商 ${providerId} 缺少 API 地址(baseURL),模型 ${modelId} 无法下发。` +
100
+ '请到【设置-模型】补全该供应商的 API 地址。',
101
+ );
102
+ continue;
103
+ }
104
+ upstreams.set(modelId, {
105
+ name: modelId,
106
+ providerId,
107
+ prefix: protocolPrefix(prov),
108
+ baseUrl: prov.baseUrl,
109
+ keyEnv: keyEnvName(prov),
110
+ timeoutSec: Math.max(1, Math.round((cfg.timeoutMs || 600000) / 1000)),
111
+ });
112
+ }
113
+ }
114
+ }
115
+
116
+ // ---- 2. 生成 YAML -------------------------------------------------------
117
+ const lines = [];
118
+ const push = (s = '') => lines.push(s);
119
+
120
+ push('# ============================================================================');
121
+ push('# Generated by dsh-plugin-litellm-bridge - profile fallback chains');
122
+ push(`# Generated at: ${new Date().toISOString()}`);
123
+ push('# ============================================================================');
124
+ push('# Model catalog and provider credentials live in DSH Settings > Models.');
125
+ push('# This file only mirrors those upstreams so the gateway can execute the');
126
+ push('# fallback order. The plugin itself performs no routing / retry / fallback.');
127
+ push('# api_key entries are os.environ references - never plaintext.');
128
+ push('#');
129
+
130
+ push('model_list:');
131
+ for (const u of upstreams.values()) {
132
+ push(` # ${u.providerId}`);
133
+ push(` - model_name: ${u.name}`);
134
+ push(' litellm_params:');
135
+ push(` model: ${u.prefix}/${u.name}`);
136
+ push(` api_base: ${u.baseUrl}`);
137
+ push(` api_key: os.environ/${u.keyEnv}`);
138
+ push(` timeout: ${u.timeoutSec}`);
139
+ }
140
+ // 链头:逻辑名 → 链首模型的真实上游
141
+ for (const c of chains) {
142
+ const first = upstreams.get(c.chain[0]);
143
+ if (!first) continue;
144
+ if (upstreams.has(c.head)) continue;
145
+ push(` # chain head: ${c.pid} / ${c.at}`);
146
+ push(` - model_name: ${c.head}`);
147
+ push(' litellm_params:');
148
+ push(` model: ${first.prefix}/${first.name}`);
149
+ push(` api_base: ${first.baseUrl}`);
150
+ push(` api_key: os.environ/${first.keyEnv}`);
151
+ push(` timeout: ${Math.max(1, Math.round(c.timeoutMs / 1000))}`);
152
+ }
153
+ push('');
154
+
155
+ // ---- 3. 降级顺序(网关执行)--------------------------------------------
156
+ push('litellm_settings:');
157
+ push(' # Fallback order is assembled by the plugin, executed by the gateway.');
158
+ const multi = chains.filter((c) => c.chain.length >= 2 && upstreams.has(c.chain[0]));
159
+ push(' fallbacks:');
160
+ if (!multi.length) {
161
+ push(' []');
162
+ } else {
163
+ for (const c of multi) push(` - ${c.head}: [${c.chain.slice(1).map((m) => `"${m}"`).join(', ')}]`);
164
+ }
165
+ push(' context_window_fallbacks:');
166
+ if (!multi.length) {
167
+ push(' []');
168
+ } else {
169
+ for (const c of multi) push(` - ${c.head}: ["${c.chain[c.chain.length - 1]}"]`);
170
+ }
171
+ push(' num_retries: 2');
172
+ push(' request_timeout: 900');
173
+ push(' drop_params: true');
174
+ push(' forward_client_headers_to_llm_api: true # pass x-agent-type / x-session-profile through');
175
+ push('');
176
+
177
+ return {
178
+ yaml: lines.join('\n'),
179
+ warnings: [...new Set(warnings)],
180
+ chains: chains.map(({ head, pid, at, chain }) => ({ head, pid, at, chain })),
181
+ upstreams: [...upstreams.values()],
182
+ };
183
+ }
184
+
185
+ /** 兼容旧签名:只要 YAML 文本。 */
186
+ export function assembleChainYaml(state, pool = null) {
187
+ return assembleChainConfig(state, pool).yaml;
188
+ }
189
+
190
+ /** 排障提示用:模型名 → 约定环境变量名(不再写入配置)。 */
191
+ export function upstreamEnvVar(modelId) {
192
+ return `LITELLM_UPSTREAM_${String(modelId).toUpperCase().replace(/[^A-Z0-9]/g, '_')}`;
193
+ }
@@ -0,0 +1,290 @@
1
+ /**
2
+ * 插件配置与路由 Profile 的持久化
3
+ * ============================================================================
4
+ *
5
+ * 【架构修正后的职责】
6
+ * - 存网关连接设置(地址、超时、日志条数)
7
+ * - 存 profile 列表:每个 profile 下 root / coder / research 三条降级链
8
+ * (root = 主 Agent 链;coder / research = 子 Agent 链,两者可完全不同)
9
+ *
10
+ * 【模型来源变了】模型名不再来自 LiteLLM 网关(那个 models/sync 已删除),
11
+ * 而是用户在 UI 里从 **DSH【设置-模型】的模型池** 里选出来的。
12
+ * 本模块只存"选了哪些模型、什么顺序",不碰任何供应商凭证。
13
+ *
14
+ * 【绝不存密钥】真正的 master key 在网关侧 .env 里;插件只在内存里按需读取。
15
+ *
16
+ * 落盘位置:$DSH_HOME/litellm-bridge/state.json
17
+ */
18
+
19
+ import { mkdir, readFile, writeFile, rename } from 'node:fs/promises';
20
+ import { homedir } from 'node:os';
21
+ import { dirname, join } from 'node:path';
22
+
23
+ /** 三类 Agent 槽位。root 是主链,coder/research 是子 Agent 链。 */
24
+ export const AGENT_TYPES = ['root', 'coder', 'research'];
25
+
26
+ /** 默认 profile id(用户可在 UI 里新建更多)。 */
27
+ export const DEFAULT_PROFILE_ID = 'profile-a';
28
+
29
+ /** 兼容旧代码引用:默认 profile 列表。 */
30
+ export const PROFILE_IDS = ['profile-a', 'profile-b'];
31
+
32
+ /**
33
+ * 校验 profile id:只允许字母数字、连字符、下划线,长度 1–40。
34
+ * 因为它会被写进 YAML 的 model_name,必须保证 YAML/ASCII 安全。
35
+ */
36
+ export function isValidProfileId(id) {
37
+ return typeof id === 'string' && /^[A-Za-z0-9][A-Za-z0-9_-]{0,39}$/.test(id);
38
+ }
39
+
40
+ /** 一个空的 agent 链骨架。 */
41
+ export function emptyAgentChain(label = '') {
42
+ return { label, primary: '', timeoutMs: 600000, fallbacks: [] };
43
+ }
44
+
45
+ /** 一个空 profile 骨架。 */
46
+ export function emptyProfile(id) {
47
+ return {
48
+ label: id,
49
+ agents: {
50
+ root: emptyAgentChain('主代理'),
51
+ coder: emptyAgentChain('子代理 · Coder'),
52
+ research: emptyAgentChain('子代理 · Research'),
53
+ },
54
+ };
55
+ }
56
+
57
+ /**
58
+ * 出厂默认:两个 profile 骨架。
59
+ *
60
+ * 【注意】primary/fallbacks 故意留空:模型必须由用户从 DSH 模型池里选,
61
+ * 插件不能凭空编造模型名(旧版写死了 root-primary 之类网关侧别名,
62
+ * 现在网关不再维护模型清单,那些名字已经没有意义)。
63
+ */
64
+ export function defaultRoutes() {
65
+ return {
66
+ 'profile-a': emptyProfile('profile-a(稳定优先)'),
67
+ 'profile-b': emptyProfile('profile-b(成本优先)'),
68
+ };
69
+ }
70
+
71
+ export function defaultState() {
72
+ return {
73
+ version: 2,
74
+ gateway: {
75
+ url: 'http://127.0.0.1:4000',
76
+ timeoutMs: 30000,
77
+ logLimit: 50,
78
+ reloadEndpoint: '',
79
+ lastProbeAt: null,
80
+ lastProbeOk: null,
81
+ },
82
+ routes: defaultRoutes(),
83
+ updatedAt: null,
84
+ };
85
+ }
86
+
87
+ export function stateDir() {
88
+ const home = process.env.DSH_HOME || join(homedir(), '.dsh');
89
+ return join(home, 'litellm-bridge');
90
+ }
91
+
92
+ export function statePath() {
93
+ return join(stateDir(), 'state.json');
94
+ }
95
+
96
+ /** 归一化一条 agent 链(容忍旧版本 / 残缺字段)。 */
97
+ function mergeAgentChain(src, base) {
98
+ const out = { ...base };
99
+ if (!src || typeof src !== 'object') return out;
100
+ if (typeof src.label === 'string' && src.label) out.label = src.label;
101
+ if (typeof src.primary === 'string') out.primary = src.primary.trim();
102
+ const t = Number(src.timeoutMs);
103
+ if (Number.isFinite(t) && t > 0) out.timeoutMs = t;
104
+ if (Array.isArray(src.fallbacks)) {
105
+ out.fallbacks = src.fallbacks
106
+ .map((x) => (typeof x === 'string' ? x.trim() : ''))
107
+ .filter(Boolean);
108
+ }
109
+ return out;
110
+ }
111
+
112
+ /** 深合并:磁盘内容覆盖默认值,但结构始终完整。 */
113
+ function mergeState(base, loaded) {
114
+ if (!loaded || typeof loaded !== 'object') return base;
115
+ const merged = {
116
+ ...base,
117
+ gateway: { ...base.gateway, ...(loaded.gateway || {}) },
118
+ routes: {},
119
+ };
120
+ const srcRoutes = loaded.routes && typeof loaded.routes === 'object' ? loaded.routes : null;
121
+ if (srcRoutes) {
122
+ for (const [pid, prof] of Object.entries(srcRoutes)) {
123
+ if (!isValidProfileId(pid)) continue;
124
+ const skeleton = emptyProfile(pid);
125
+ const out = {
126
+ label: typeof prof?.label === 'string' && prof.label ? prof.label : skeleton.label,
127
+ agents: {},
128
+ };
129
+ for (const at of AGENT_TYPES) {
130
+ out.agents[at] = mergeAgentChain(prof?.agents?.[at], skeleton.agents[at]);
131
+ }
132
+ merged.routes[pid] = out;
133
+ }
134
+ }
135
+ // 一个 profile 都没有时(旧文件为空 / 全非法)退回默认,避免 UI 空壳
136
+ if (Object.keys(merged.routes).length === 0) merged.routes = defaultRoutes();
137
+ return merged;
138
+ }
139
+
140
+ export class ConfigStore {
141
+ constructor() {
142
+ this._state = defaultState();
143
+ this._loaded = false;
144
+ this._writeChain = Promise.resolve();
145
+ }
146
+
147
+ async load() {
148
+ if (this._loaded) return this._state;
149
+ try {
150
+ const raw = await readFile(statePath(), 'utf8');
151
+ this._state = mergeState(defaultState(), JSON.parse(raw));
152
+ } catch (err) {
153
+ if (err?.code !== 'ENOENT') {
154
+ this._loadError = String(err?.message || err);
155
+ }
156
+ this._state = defaultState();
157
+ }
158
+ this._loaded = true;
159
+ return this._state;
160
+ }
161
+
162
+ /** 只读快照(UI 用)。保证绝不包含任何密钥字段。 */
163
+ get() {
164
+ const s = this._state;
165
+ return {
166
+ version: s.version,
167
+ gateway: {
168
+ url: s.gateway.url,
169
+ timeoutMs: s.gateway.timeoutMs,
170
+ logLimit: s.gateway.logLimit,
171
+ reloadEndpoint: s.gateway.reloadEndpoint,
172
+ lastProbeAt: s.gateway.lastProbeAt,
173
+ lastProbeOk: s.gateway.lastProbeOk,
174
+ keySource: '来自 .env 环境变量',
175
+ },
176
+ routes: s.routes,
177
+ profileIds: Object.keys(s.routes),
178
+ updatedAt: s.updatedAt,
179
+ statePath: statePath(),
180
+ loadError: this._loadError || null,
181
+ };
182
+ }
183
+
184
+ /** 串行化写入 + 原子替换,避免并发保存把文件写坏。 */
185
+ async _persist() {
186
+ this._state.updatedAt = new Date().toISOString();
187
+ const snapshot = JSON.stringify(this._state, null, 2);
188
+ const target = statePath();
189
+ this._writeChain = this._writeChain.then(async () => {
190
+ await mkdir(dirname(target), { recursive: true });
191
+ const tmp = `${target}.${process.pid}.tmp`;
192
+ await writeFile(tmp, snapshot, 'utf8');
193
+ await rename(tmp, target);
194
+ });
195
+ return this._writeChain;
196
+ }
197
+
198
+ async updateGateway(patch = {}) {
199
+ const g = this._state.gateway;
200
+ if (typeof patch.url === 'string') g.url = patch.url.trim().replace(/\/+$/, '');
201
+ if (Number(patch.timeoutMs) > 0) g.timeoutMs = Number(patch.timeoutMs);
202
+ if (Number(patch.logLimit) > 0) g.logLimit = Math.min(500, Number(patch.logLimit));
203
+ if (typeof patch.reloadEndpoint === 'string') g.reloadEndpoint = patch.reloadEndpoint;
204
+ await this._persist();
205
+ return this.get();
206
+ }
207
+
208
+ async markProbe(okFlag) {
209
+ this._state.gateway.lastProbeAt = new Date().toISOString();
210
+ this._state.gateway.lastProbeOk = Boolean(okFlag);
211
+ await this._persist();
212
+ return this.get();
213
+ }
214
+
215
+ /** 新建 profile。已存在则报错(不静默覆盖用户配置)。 */
216
+ async createProfile(id, label = '') {
217
+ if (!isValidProfileId(id)) {
218
+ throw new Error(`profile 名称非法:只能用字母数字、连字符、下划线(1–40 字符)`);
219
+ }
220
+ if (this._state.routes[id]) throw new Error(`profile 已存在:${id}`);
221
+ this._state.routes[id] = emptyProfile(label || id);
222
+ await this._persist();
223
+ return this.get();
224
+ }
225
+
226
+ /** 重命名 profile 的显示标签(id 不改,避免破坏会话绑定)。 */
227
+ async renameProfile(id, label) {
228
+ const prof = this._state.routes[id];
229
+ if (!prof) throw new Error(`未知 profile: ${id}`);
230
+ if (typeof label === 'string') prof.label = label.trim() || id;
231
+ await this._persist();
232
+ return this.get();
233
+ }
234
+
235
+ /** 删除 profile(至少保留一个,否则会话绑定的值会指向不存在的链)。 */
236
+ async deleteProfile(id) {
237
+ if (!this._state.routes[id]) throw new Error(`未知 profile: ${id}`);
238
+ if (Object.keys(this._state.routes).length <= 1) {
239
+ throw new Error('至少保留一个 profile');
240
+ }
241
+ delete this._state.routes[id];
242
+ await this._persist();
243
+ return this.get();
244
+ }
245
+
246
+ /**
247
+ * 覆盖某个 profile 下某类 agent 的配置。
248
+ * fallbacks 用「有序数组」表达拖拽排序结果——顺序即优先级。
249
+ */
250
+ async updateRoute(profileId, agentType, patch = {}) {
251
+ const prof = this._state.routes[profileId];
252
+ if (!prof) throw new Error(`未知 profile: ${profileId}`);
253
+ if (!AGENT_TYPES.includes(agentType)) throw new Error(`未知 agent 类型: ${agentType}`);
254
+ const target = prof.agents[agentType];
255
+ if (typeof patch.primary === 'string') target.primary = patch.primary.trim();
256
+ if (Number(patch.timeoutMs) > 0) target.timeoutMs = Number(patch.timeoutMs);
257
+ if (Array.isArray(patch.fallbacks)) {
258
+ target.fallbacks = patch.fallbacks
259
+ .map((x) => (typeof x === 'string' ? x.trim() : ''))
260
+ .filter(Boolean);
261
+ }
262
+ if (typeof patch.label === 'string' && patch.label) target.label = patch.label;
263
+ await this._persist();
264
+ return this.get();
265
+ }
266
+
267
+ /** 整体替换某 profile 的 agent 配置(UI 保存整块时用)。 */
268
+ async replaceProfile(profileId, agents = {}) {
269
+ if (!this._state.routes[profileId]) throw new Error(`未知 profile: ${profileId}`);
270
+ for (const at of AGENT_TYPES) {
271
+ if (agents[at]) await this.updateRoute(profileId, at, agents[at]);
272
+ }
273
+ return this.get();
274
+ }
275
+
276
+ /** 重置所有路由为出厂骨架(不动网关设置)。 */
277
+ async resetRoutes() {
278
+ this._state.routes = defaultRoutes();
279
+ await this._persist();
280
+ return this.get();
281
+ }
282
+ }
283
+
284
+ /**
285
+ * 【已废弃】旧的 assembleYaml —— 组装逻辑已迁移到 assemble-chain.js。
286
+ *
287
+ * 注意:新版的 assembleChainYaml 需要「模型池」作为第二参数才能解析出
288
+ * 供应商的 api_base 与凭证环境变量名。只传 state 时上游会缺 api_base,
289
+ * 所以这里不再提供无池转发(避免调用方以为配置可用、实际调不通)。
290
+ */