@yeaft/webchat-agent 0.1.943 → 0.1.945

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/cli.js CHANGED
@@ -11,6 +11,15 @@ import { readFileSync, writeFileSync, mkdirSync } from 'fs';
11
11
  import { dirname, join } from 'path';
12
12
  import { fileURLToPath } from 'url';
13
13
  import { platform, homedir } from 'os';
14
+ import {
15
+ addOrUpdateProvider,
16
+ formatLlmConfig,
17
+ getDefaultYeaftConfigPath,
18
+ readLocalLlmConfig,
19
+ removeProvider,
20
+ setLocalModels,
21
+ writeLocalLlmConfig,
22
+ } from './llm-config-cli.js';
14
23
 
15
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
16
25
  const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
@@ -24,6 +33,8 @@ const SERVICE_COMMANDS = ['install', 'uninstall', 'start', 'stop', 'restart', 's
24
33
 
25
34
  if (command === 'doctor') {
26
35
  handleDoctorCommand();
36
+ } else if (command === 'llm') {
37
+ handleLlmCommand(subArgs);
27
38
  } else if (command === 'upgrade') {
28
39
  upgrade();
29
40
  } else if (command === '--version' || command === '-v') {
@@ -51,6 +62,7 @@ function printHelp() {
51
62
  yeaft-agent status Show service status
52
63
  yeaft-agent logs View service logs (follow mode)
53
64
  yeaft-agent doctor Diagnose service configuration
65
+ yeaft-agent llm <command> Configure local Yeaft LLM providers/models
54
66
  yeaft-agent upgrade Upgrade to latest version
55
67
  yeaft-agent --version Show version
56
68
 
@@ -75,6 +87,104 @@ function printHelp() {
75
87
  `);
76
88
  }
77
89
 
90
+ function printLlmHelp() {
91
+ console.log(`
92
+ Configure local Yeaft LLM providers/models in ~/.yeaft/config.json.
93
+
94
+ Usage:
95
+ yeaft-agent llm show [--reveal]
96
+ yeaft-agent llm add-provider --name <name> --base-url <url> --models <m1,m2> \
97
+ [--api-key <key>|--api-key-env <ENV>|--credential-provider github-copilot] \
98
+ [--protocol anthropic|openai-responses] [--set-primary <model>] [--set-fast <model>]
99
+ yeaft-agent llm set-model [--primary <provider/model>] [--fast <provider/model>]
100
+ yeaft-agent llm remove-provider --name <name>
101
+
102
+ Behavior:
103
+ add-provider updates/replaces an existing provider with the same --name.
104
+ --api-key-env reads the environment variable value and writes it as apiKey.
105
+ set-model requires full provider/model references.
106
+ --config <path> can target a config file for tests or scripted setup.
107
+
108
+ Examples:
109
+ OPENAI_KEY=sk-... yeaft-agent llm add-provider --name openai --base-url https://api.openai.com/v1 --models gpt-5,gpt-4.1 --api-key-env OPENAI_KEY --protocol openai-responses --set-primary gpt-5
110
+ yeaft-agent llm add-provider --name copilot --base-url https://api.githubcopilot.com --models claude-sonnet-4.5,gpt-5 --credential-provider github-copilot
111
+ yeaft-agent llm set-model --primary openai/gpt-5 --fast openai/gpt-4.1
112
+ yeaft-agent llm show --reveal
113
+ `);
114
+ }
115
+
116
+ function handleLlmCommand(args) {
117
+ const subcommand = args[0];
118
+
119
+ try {
120
+ const options = parseLlmArgs(args.slice(1));
121
+ const configPath = options.config || getDefaultYeaftConfigPath();
122
+ if (!subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help') {
123
+ printLlmHelp();
124
+ return;
125
+ }
126
+
127
+ if (subcommand === 'show') {
128
+ const config = readLocalLlmConfig(configPath);
129
+ console.log(formatLlmConfig({ ...config, __configPath: configPath }, { reveal: Boolean(options.reveal) }));
130
+ return;
131
+ }
132
+
133
+ const current = readLocalLlmConfig(configPath);
134
+ let result;
135
+ if (subcommand === 'add-provider') {
136
+ result = addOrUpdateProvider(current, options, process.env);
137
+ writeLocalLlmConfig(result.config, configPath);
138
+ console.log(`${result.replaced ? 'Updated' : 'Added'} provider: ${result.provider.name}`);
139
+ if (result.config.primaryModel) console.log(`Primary model: ${result.config.primaryModel}`);
140
+ if (result.config.fastModel) console.log(`Fast model: ${result.config.fastModel}`);
141
+ return;
142
+ }
143
+
144
+ if (subcommand === 'set-model') {
145
+ result = setLocalModels(current, options);
146
+ writeLocalLlmConfig(result.config, configPath);
147
+ if (result.config.primaryModel) console.log(`Primary model: ${result.config.primaryModel}`);
148
+ if (result.config.fastModel) console.log(`Fast model: ${result.config.fastModel}`);
149
+ return;
150
+ }
151
+
152
+ if (subcommand === 'remove-provider') {
153
+ result = removeProvider(current, options);
154
+ writeLocalLlmConfig(result.config, configPath);
155
+ console.log(result.removed ? `Removed provider: ${options.name}` : `Provider not found: ${options.name}`);
156
+ if (result.cleared.length) {
157
+ console.log(`Cleared ${result.cleared.join(', ')} because it referenced ${options.name}`);
158
+ }
159
+ return;
160
+ }
161
+
162
+ throw new Error(`Unknown llm command: ${subcommand}`);
163
+ } catch (err) {
164
+ console.error(`Error: ${err.message}`);
165
+ console.error('Run `yeaft-agent llm --help` for usage.');
166
+ process.exit(1);
167
+ }
168
+ }
169
+
170
+ function parseLlmArgs(args) {
171
+ const options = {};
172
+ for (let i = 0; i < args.length; i++) {
173
+ const arg = args[i];
174
+ if (arg === '--reveal') {
175
+ options.reveal = true;
176
+ continue;
177
+ }
178
+ const key = arg.startsWith('--') ? arg.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase()) : null;
179
+ if (!key) throw new Error(`Unexpected argument: ${arg}`);
180
+ const value = args[i + 1];
181
+ if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value`);
182
+ options[key] = value;
183
+ i += 1;
184
+ }
185
+ return options;
186
+ }
187
+
78
188
  async function handleServiceCommand(command, args) {
79
189
  const service = await import('./service.js');
80
190
  switch (command) {
@@ -335,27 +335,9 @@ export async function handleMessage(msg) {
335
335
  break;
336
336
  }
337
337
 
338
- // LLM configuration (read/write ~/.yeaft/config.json)
338
+ // LLM configuration (read/write this agent's ~/.yeaft/config.json)
339
339
  case 'get_llm_config': {
340
- if (msg.globalConfig && typeof msg.globalConfig === 'object') {
341
- ctx.globalLlmConfig = msg.globalConfig;
342
- }
343
- const config = getLlmConfig(ctx.CONFIG?.yeaftDir, ctx.globalLlmConfig);
344
- sendToServer({ type: 'llm_config', ...config });
345
- break;
346
- }
347
-
348
- case 'llm_global_config_updated': {
349
- ctx.globalLlmConfig = msg.globalConfig && typeof msg.globalConfig === 'object'
350
- ? msg.globalConfig
351
- : { providers: [] };
352
- // Existing sessions cache AdapterRouter instances. Drop them so the
353
- // next Yeaft turn sees the new global providers, without writing them
354
- // to the node-local config.json.
355
- resetYeaftSession().catch(err => {
356
- console.error('[LLM] Failed to reload Yeaft session after global config update:', err.message);
357
- });
358
- const config = getLlmConfig(ctx.CONFIG?.yeaftDir, ctx.globalLlmConfig);
340
+ const config = getLlmConfig(ctx.CONFIG?.yeaftDir);
359
341
  sendToServer({ type: 'llm_config', ...config });
360
342
  break;
361
343
  }
@@ -369,10 +351,7 @@ export async function handleMessage(msg) {
369
351
  const incomingLanguage = typeof msg.config?.language === 'string' && msg.config.language
370
352
  ? msg.config.language
371
353
  : null;
372
- if (msg.globalConfig && typeof msg.globalConfig === 'object') {
373
- ctx.globalLlmConfig = msg.globalConfig;
374
- }
375
- const result = updateLlmConfig(msg.config || {}, ctx.CONFIG?.yeaftDir, ctx.globalLlmConfig);
354
+ const result = updateLlmConfig(msg.config || {}, ctx.CONFIG?.yeaftDir);
376
355
  // task-708: live locale propagation. When the user flips the UI
377
356
  // language dropdown, push the new value into every cached Engine
378
357
  // (per-VP pool + 1:1 chat session.engine) so the very next turn
package/context.js CHANGED
@@ -18,8 +18,6 @@ export default {
18
18
  slashCommandDescriptions: {},
19
19
  // MCP servers 列表 (从 ~/.claude.json 读取): [{ name, enabled, source }]
20
20
  mcpServers: [],
21
- // Server-owned user-global LLM providers. Runtime-only: never persisted to ~/.yeaft/config.json.
22
- globalLlmConfig: { providers: [] },
23
21
  // 连接相关
24
22
  reconnectTimer: null,
25
23
  pendingAuthTempId: null,
@@ -0,0 +1,196 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { dirname, join } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ const VALID_PROTOCOLS = new Set(['anthropic', 'openai-responses']);
6
+ const VALID_CREDENTIAL_PROVIDERS = new Set(['github-copilot']);
7
+
8
+ export function getDefaultYeaftConfigPath() {
9
+ return join(homedir(), '.yeaft', 'config.json');
10
+ }
11
+
12
+ export function readLocalLlmConfig(configPath = getDefaultYeaftConfigPath()) {
13
+ if (!existsSync(configPath)) return {};
14
+ const raw = readFileSync(configPath, 'utf8');
15
+ if (!raw.trim()) return {};
16
+ const parsed = JSON.parse(raw);
17
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
18
+ throw new Error(`Invalid config file: expected JSON object at ${configPath}`);
19
+ }
20
+ return parsed;
21
+ }
22
+
23
+ export function writeLocalLlmConfig(config, configPath = getDefaultYeaftConfigPath()) {
24
+ mkdirSync(dirname(configPath), { recursive: true });
25
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
26
+ }
27
+
28
+ export function parseModelsCsv(value) {
29
+ if (!value || typeof value !== 'string') {
30
+ throw new Error('--models is required and must be a comma-separated list');
31
+ }
32
+ const models = value.split(',').map(s => s.trim()).filter(Boolean);
33
+ if (models.length === 0) {
34
+ throw new Error('--models must include at least one model id');
35
+ }
36
+ return models;
37
+ }
38
+
39
+ export function maskApiKey(value) {
40
+ if (!value) return value;
41
+ if (value.length <= 8) return '********';
42
+ return `${value.slice(0, 4)}…${value.slice(-4)}`;
43
+ }
44
+
45
+ export function formatLlmConfig(config, { reveal = false } = {}) {
46
+ const providers = Array.isArray(config.providers) ? config.providers : [];
47
+ const lines = [];
48
+ lines.push(`Config: ${config.__configPath || getDefaultYeaftConfigPath()}`);
49
+ lines.push(`Primary model: ${config.primaryModel || '(not set)'}`);
50
+ lines.push(`Fast model: ${config.fastModel || '(not set)'}`);
51
+ lines.push('Providers:');
52
+
53
+ if (providers.length === 0) {
54
+ lines.push(' (none)');
55
+ return lines.join('\n');
56
+ }
57
+
58
+ for (const provider of providers) {
59
+ lines.push(` - ${provider.name}`);
60
+ lines.push(` baseUrl: ${provider.baseUrl || '(not set)'}`);
61
+ if (provider.protocol) lines.push(` protocol: ${provider.protocol}`);
62
+ if (provider.credentialProvider) {
63
+ lines.push(` credentialProvider: ${provider.credentialProvider}`);
64
+ } else if (provider.apiKey) {
65
+ lines.push(` apiKey: ${reveal ? provider.apiKey : maskApiKey(provider.apiKey)}`);
66
+ } else {
67
+ lines.push(' apiKey: (not set)');
68
+ }
69
+ const models = Array.isArray(provider.models) ? provider.models.map(formatModelEntry) : [];
70
+ lines.push(` models: ${models.length ? models.join(', ') : '(none)'}`);
71
+ }
72
+
73
+ return lines.join('\n');
74
+ }
75
+
76
+ function formatModelEntry(model) {
77
+ if (typeof model === 'string') return model;
78
+ if (model && typeof model === 'object' && model.id) {
79
+ return model.protocol ? `${model.id} (${model.protocol})` : model.id;
80
+ }
81
+ return String(model);
82
+ }
83
+
84
+ export function addOrUpdateProvider(config, options, env = process.env) {
85
+ const name = requireNonEmpty(options.name, '--name');
86
+ const baseUrl = requireNonEmpty(options.baseUrl, '--base-url');
87
+ const models = parseModelsCsv(options.models);
88
+ validateProtocol(options.protocol);
89
+ validateCredentials(options, env);
90
+
91
+ const next = { ...config };
92
+ const providers = Array.isArray(config.providers) ? [...config.providers] : [];
93
+ const provider = {
94
+ name,
95
+ baseUrl,
96
+ models,
97
+ };
98
+ if (options.protocol) provider.protocol = options.protocol;
99
+ if (options.credentialProvider) provider.credentialProvider = options.credentialProvider;
100
+ if (options.apiKey) provider.apiKey = options.apiKey;
101
+ if (options.apiKeyEnv) provider.apiKey = env[options.apiKeyEnv];
102
+
103
+ const index = providers.findIndex(p => p && p.name === name);
104
+ if (index >= 0) providers[index] = provider;
105
+ else providers.push(provider);
106
+ next.providers = providers;
107
+
108
+ if (options.setPrimary) next.primaryModel = qualifyModelRef(options.setPrimary, name);
109
+ if (options.setFast) next.fastModel = qualifyModelRef(options.setFast, name);
110
+
111
+ return {
112
+ config: next,
113
+ replaced: index >= 0,
114
+ provider,
115
+ };
116
+ }
117
+
118
+ export function setLocalModels(config, options) {
119
+ const hasPrimary = Boolean(options.primary);
120
+ const hasFast = Boolean(options.fast);
121
+ if (!hasPrimary && !hasFast) {
122
+ throw new Error('set-model requires --primary and/or --fast');
123
+ }
124
+ const next = { ...config };
125
+ if (hasPrimary) next.primaryModel = requireFullModelRef(options.primary, '--primary');
126
+ if (hasFast) next.fastModel = requireFullModelRef(options.fast, '--fast');
127
+ return { config: next };
128
+ }
129
+
130
+ export function removeProvider(config, options) {
131
+ const name = requireNonEmpty(options.name, '--name');
132
+ const next = { ...config };
133
+ const providers = Array.isArray(config.providers) ? config.providers : [];
134
+ const kept = providers.filter(p => !p || p.name !== name);
135
+ const removed = kept.length !== providers.length;
136
+ next.providers = kept;
137
+
138
+ const cleared = [];
139
+ if (pointsAtProvider(next.primaryModel, name)) {
140
+ delete next.primaryModel;
141
+ cleared.push('primaryModel');
142
+ }
143
+ if (pointsAtProvider(next.fastModel, name)) {
144
+ delete next.fastModel;
145
+ cleared.push('fastModel');
146
+ }
147
+
148
+ return { config: next, removed, cleared };
149
+ }
150
+
151
+ export function qualifyModelRef(model, providerName) {
152
+ const value = requireNonEmpty(model, 'model');
153
+ return value.includes('/') ? value : `${providerName}/${value}`;
154
+ }
155
+
156
+ function requireFullModelRef(model, flagName) {
157
+ const value = requireNonEmpty(model, flagName);
158
+ if (!value.includes('/')) {
159
+ throw new Error(`${flagName} must be a full provider/model reference`);
160
+ }
161
+ return value;
162
+ }
163
+
164
+ function pointsAtProvider(modelRef, providerName) {
165
+ return typeof modelRef === 'string' && modelRef.startsWith(`${providerName}/`);
166
+ }
167
+
168
+ function requireNonEmpty(value, label) {
169
+ if (typeof value !== 'string' || value.trim() === '') {
170
+ throw new Error(`${label} is required`);
171
+ }
172
+ return value.trim();
173
+ }
174
+
175
+ function validateProtocol(protocol) {
176
+ if (protocol && !VALID_PROTOCOLS.has(protocol)) {
177
+ throw new Error(`--protocol must be one of: ${Array.from(VALID_PROTOCOLS).join(', ')}`);
178
+ }
179
+ }
180
+
181
+ function validateCredentials(options, env) {
182
+ const credentialCount = [options.apiKey, options.apiKeyEnv, options.credentialProvider]
183
+ .filter(Boolean).length;
184
+ if (credentialCount > 1) {
185
+ throw new Error('--api-key, --api-key-env, and --credential-provider are mutually exclusive');
186
+ }
187
+ if (options.credentialProvider && !VALID_CREDENTIAL_PROVIDERS.has(options.credentialProvider)) {
188
+ throw new Error(`--credential-provider must be one of: ${Array.from(VALID_CREDENTIAL_PROVIDERS).join(', ')}`);
189
+ }
190
+ if (options.apiKeyEnv) {
191
+ const envName = options.apiKeyEnv;
192
+ if (!env[envName]) {
193
+ throw new Error(`Environment variable ${envName} is not set`);
194
+ }
195
+ }
196
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.943",
3
+ "version": "0.1.945",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -13,7 +13,6 @@ import { join } from 'path';
13
13
  import { DEFAULT_YEAFT_DIR } from './init.js';
14
14
  import { normalizeProviderModels, serializeModelForPersistence } from './models.js';
15
15
  import { normaliseYeaftSection } from './config.js';
16
- import { mergeLlmConfigs } from './llm/provider-merge.js';
17
16
 
18
17
  /**
19
18
  * Read the LLM-relevant portion of config.json.
@@ -41,15 +40,13 @@ function readLocalLlmConfig(dir) {
41
40
  };
42
41
  }
43
42
 
44
- export function getLlmConfig(dir, globalConfig = {}) {
43
+ export function getLlmConfig(dir) {
45
44
  try {
46
45
  const agentConfig = readLocalLlmConfig(dir);
47
- const effectiveConfig = mergeLlmConfigs(globalConfig, agentConfig);
48
46
  return {
49
- ...effectiveConfig,
47
+ ...agentConfig,
50
48
  agentConfig,
51
- effectiveConfig,
52
- globalConfig: { providers: Array.isArray(globalConfig.providers) ? globalConfig.providers : [] },
49
+ effectiveConfig: agentConfig,
53
50
  };
54
51
  } catch (e) {
55
52
  return { error: `Failed to read config.json: ${e.message}` };
@@ -64,7 +61,7 @@ export function getLlmConfig(dir, globalConfig = {}) {
64
61
  * @param {string} [dir] — Yeaft data directory
65
62
  * @returns {{ providers, primaryModel, fastModel, language } | { error: string }}
66
63
  */
67
- export function updateLlmConfig(update, dir, globalConfig = {}) {
64
+ export function updateLlmConfig(update, dir) {
68
65
  const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
69
66
  const configPath = join(root, 'config.json');
70
67
 
@@ -132,12 +129,10 @@ export function updateLlmConfig(update, dir, globalConfig = {}) {
132
129
  fastModel: existing.fastModel || null,
133
130
  language: existing.language || 'en',
134
131
  };
135
- const effectiveConfig = mergeLlmConfigs(globalConfig, agentConfig);
136
132
  return {
137
- ...effectiveConfig,
133
+ ...agentConfig,
138
134
  agentConfig,
139
- effectiveConfig,
140
- globalConfig: { providers: Array.isArray(globalConfig.providers) ? globalConfig.providers : [] },
135
+ effectiveConfig: agentConfig,
141
136
  };
142
137
  }
143
138
 
package/yeaft/config.js CHANGED
@@ -23,7 +23,6 @@ import { existsSync, readFileSync } from 'fs';
23
23
  import { join } from 'path';
24
24
  import { DEFAULT_YEAFT_DIR } from './init.js';
25
25
  import { resolveModel, parseModelRef, normalizeProviderModels, resolveContextWindow, resolveMaxOutputTokens } from './models.js';
26
- import { mergeLlmConfigs } from './llm/provider-merge.js';
27
26
 
28
27
  /** Default configuration values. */
29
28
  const DEFAULTS = {
@@ -295,39 +294,36 @@ export function loadConfig(overrides = {}) {
295
294
  }
296
295
 
297
296
  // ─── Build config from config.json ────────────────────────
298
- const agentProviders = Array.isArray(jsonConfig.providers) ? jsonConfig.providers : [];
299
- const mergedLlmConfig = mergeLlmConfigs(overrides.globalLlmConfig || {}, {
300
- providers: agentProviders,
301
- primaryModel: jsonConfig.primaryModel || null,
302
- fastModel: jsonConfig.fastModel || null,
303
- language: jsonConfig.language || DEFAULTS.language,
304
- });
305
- const providers = mergedLlmConfig.providers;
297
+ const providers = Array.isArray(jsonConfig.providers) ? jsonConfig.providers : [];
306
298
 
307
299
  // Resolve primary model
308
300
  let model = 'claude-sonnet-4-20250514';
309
301
  let modelIdForInfo = model;
310
- let primaryModel = mergedLlmConfig.primaryModel || null;
302
+ let primaryModel = jsonConfig.primaryModel || null;
311
303
  if (primaryModel) {
312
304
  const parsed = parseModelRef(primaryModel);
313
- model = parsed.providerName?.startsWith('global:') ? primaryModel : parsed.modelId;
305
+ // Global provider refs were removed. Old local config may still contain
306
+ // `global:<provider>/<model>` from previous UI versions; strip the dead
307
+ // namespace so runtime routing can match agent-local providers by model id.
308
+ const isRemovedGlobalRef = parsed.providerName?.startsWith('global:');
309
+ model = parsed.modelId;
310
+ if (isRemovedGlobalRef) primaryModel = parsed.modelId;
314
311
  modelIdForInfo = parsed.modelId;
315
312
  }
316
313
 
317
314
  // Resolve fast model
318
- let fastModel = mergedLlmConfig.fastModel || primaryModel || null;
315
+ let fastModel = jsonConfig.fastModel || primaryModel || null;
319
316
  let fastModelId = null;
320
317
  if (fastModel) {
321
318
  const parsed = parseModelRef(fastModel);
322
- fastModelId = parsed.providerName?.startsWith('global:') ? fastModel : parsed.modelId;
319
+ fastModel = parsed.providerName?.startsWith('global:') ? parsed.modelId : fastModel;
320
+ fastModelId = parsed.modelId;
323
321
  }
324
322
 
325
323
  // Resolve model info for adapter/baseUrl/thinking metadata. Token limits
326
324
  // (contextWindow / maxOutputTokens) are NOT read from here — they live in
327
325
  // models.dev and are resolved via resolveContextWindow / resolveMaxOutputTokens
328
326
  // a few lines below so the live models.dev snapshot is the source of truth.
329
- // For disambiguated global provider refs (`global:<provider>/<model>`), keep
330
- // the runtime model ref intact above but resolve metadata by the raw model id.
331
327
  const modelInfo = resolveModel(modelIdForInfo);
332
328
 
333
329
  // Pre-resolve token limits once so we can both write them onto config and
@@ -2716,7 +2716,6 @@ async function ensureSessionLoaded() {
2716
2716
  const yeaftDir = ctx.CONFIG?.yeaftDir;
2717
2717
  session = await loadSession({
2718
2718
  ...(yeaftDir && { dir: yeaftDir }),
2719
- configOverrides: { globalLlmConfig: ctx.globalLlmConfig || { providers: [] } },
2720
2719
  skipMCP: false,
2721
2720
  skipSkills: false,
2722
2721
  serverMode: true,
@@ -3842,7 +3841,6 @@ export async function handleYeaftLoadHistory(msg) {
3842
3841
  const yeaftDir = ctx.CONFIG?.yeaftDir;
3843
3842
  session = await loadSession({
3844
3843
  ...(yeaftDir && { dir: yeaftDir }),
3845
- configOverrides: { globalLlmConfig: ctx.globalLlmConfig || { providers: [] } },
3846
3844
  skipMCP: false,
3847
3845
  skipSkills: false,
3848
3846
  serverMode: true,
@@ -4158,7 +4156,6 @@ export async function resetYeaftSession() {
4158
4156
  const yeaftDir = ctx.CONFIG?.yeaftDir;
4159
4157
  session = await loadSession({
4160
4158
  ...(yeaftDir && { dir: yeaftDir }),
4161
- configOverrides: { globalLlmConfig: ctx.globalLlmConfig || { providers: [] } },
4162
4159
  skipMCP: false,
4163
4160
  skipSkills: false,
4164
4161
  serverMode: true,
@@ -1,53 +0,0 @@
1
- function cloneProvider(provider, scope) {
2
- return {
3
- ...provider,
4
- scope,
5
- source: scope,
6
- originalName: provider.name,
7
- models: Array.isArray(provider.models)
8
- ? provider.models.map(m => (m && typeof m === 'object' ? { ...m } : m))
9
- : [],
10
- };
11
- }
12
-
13
- function modelId(entry) {
14
- if (typeof entry === 'string') return entry;
15
- if (entry && typeof entry === 'object' && typeof entry.id === 'string') return entry.id;
16
- return '';
17
- }
18
-
19
- export function buildModelRef(provider, entry) {
20
- const id = modelId(entry);
21
- return provider?.name && id ? `${provider.name}/${id}` : id;
22
- }
23
-
24
- export function mergeLlmConfigs(globalConfig = {}, agentConfig = {}) {
25
- const agentProviders = Array.isArray(agentConfig.providers)
26
- ? agentConfig.providers.map(p => cloneProvider(p, 'agent'))
27
- : [];
28
- const localNames = new Set(agentProviders.map(p => p.name).filter(Boolean));
29
- const usedNames = new Set(localNames);
30
- const globalProviders = [];
31
-
32
- for (const raw of Array.isArray(globalConfig.providers) ? globalConfig.providers : []) {
33
- if (!raw?.name) continue;
34
- const provider = cloneProvider(raw, 'global');
35
- if (usedNames.has(provider.name)) {
36
- let candidate = `global:${provider.name}`;
37
- let i = 2;
38
- while (usedNames.has(candidate)) candidate = `global:${provider.name}:${i++}`;
39
- provider.name = candidate;
40
- }
41
- usedNames.add(provider.name);
42
- globalProviders.push(provider);
43
- }
44
-
45
- const providers = [...globalProviders, ...agentProviders];
46
- return {
47
- providers,
48
- primaryModel: agentConfig.primaryModel || null,
49
- fastModel: agentConfig.fastModel || null,
50
- language: agentConfig.language || 'en',
51
- needsSetup: providers.length === 0 || providers.every(p => (!p.apiKey || p.apiKey === 'proxy') && !p.credentialProvider && !p.githubToken),
52
- };
53
- }