@yeaft/webchat-agent 0.1.942 → 0.1.944
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 +110 -0
- package/connection/message-router.js +3 -24
- package/context.js +0 -2
- package/llm-config-cli.js +196 -0
- package/package.json +1 -1
- package/yeaft/config-api.js +6 -11
- package/yeaft/config.js +11 -15
- package/yeaft/web-bridge.js +0 -3
- package/yeaft/llm/provider-merge.js +0 -53
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
|
-
|
|
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
|
-
|
|
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
package/yeaft/config-api.js
CHANGED
|
@@ -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
|
|
43
|
+
export function getLlmConfig(dir) {
|
|
45
44
|
try {
|
|
46
45
|
const agentConfig = readLocalLlmConfig(dir);
|
|
47
|
-
const effectiveConfig = mergeLlmConfigs(globalConfig, agentConfig);
|
|
48
46
|
return {
|
|
49
|
-
...
|
|
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
|
|
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
|
-
...
|
|
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
|
|
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 =
|
|
302
|
+
let primaryModel = jsonConfig.primaryModel || null;
|
|
311
303
|
if (primaryModel) {
|
|
312
304
|
const parsed = parseModelRef(primaryModel);
|
|
313
|
-
|
|
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 =
|
|
315
|
+
let fastModel = jsonConfig.fastModel || primaryModel || null;
|
|
319
316
|
let fastModelId = null;
|
|
320
317
|
if (fastModel) {
|
|
321
318
|
const parsed = parseModelRef(fastModel);
|
|
322
|
-
|
|
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
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -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
|
-
}
|