@yeaft/webchat-agent 0.1.946 → 0.1.948

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
@@ -7,6 +7,8 @@ import { assertNodeVersion } from './check-node-version.js';
7
7
  assertNodeVersion({ component: '@yeaft/webchat-agent' });
8
8
 
9
9
  import { execSync, spawn } from 'child_process';
10
+ import { createInterface } from 'readline/promises';
11
+ import { stdin as input, stdout as output } from 'process';
10
12
  import { readFileSync, writeFileSync, mkdirSync } from 'fs';
11
13
  import { dirname, join } from 'path';
12
14
  import { fileURLToPath } from 'url';
@@ -18,6 +20,8 @@ import {
18
20
  readLocalLlmConfig,
19
21
  removeProvider,
20
22
  setLocalModels,
23
+ useGitHubCopilot,
24
+ useOpenAICompatible,
21
25
  writeLocalLlmConfig,
22
26
  } from './llm-config-cli.js';
23
27
 
@@ -34,7 +38,7 @@ const SERVICE_COMMANDS = ['install', 'uninstall', 'start', 'stop', 'restart', 's
34
38
  if (command === 'doctor') {
35
39
  handleDoctorCommand();
36
40
  } else if (command === 'llm') {
37
- handleLlmCommand(subArgs);
41
+ await handleLlmCommand(subArgs);
38
42
  } else if (command === 'upgrade') {
39
43
  upgrade();
40
44
  } else if (command === '--version' || command === '-v') {
@@ -93,6 +97,9 @@ function printLlmHelp() {
93
97
 
94
98
  Usage:
95
99
  yeaft-agent llm show [--reveal]
100
+ yeaft-agent llm setup
101
+ yeaft-agent llm use github-copilot --model <modelId> [--fast <modelId>] [--allow-unknown-model]
102
+ yeaft-agent llm use openai-compatible --name <name> --base-url <url> --api-key-env <ENV> --model <modelId> [--fast <modelId>]
96
103
  yeaft-agent llm add-provider --name <name> --base-url <url> --models <m1,m2> \
97
104
  [--api-key <key>|--api-key-env <ENV>|--credential-provider github-copilot] \
98
105
  [--protocol anthropic|openai-responses] [--set-primary <model>] [--set-fast <model>]
@@ -100,24 +107,27 @@ function printLlmHelp() {
100
107
  yeaft-agent llm remove-provider --name <name>
101
108
 
102
109
  Behavior:
110
+ setup/use are the recommended low-config path; add-provider is the advanced manual path.
111
+ GitHub Copilot uses the local credential provider and never writes a token to config.
103
112
  add-provider updates/replaces an existing provider with the same --name.
104
113
  --api-key-env reads the environment variable value and writes it as apiKey.
105
114
  set-model requires full provider/model references.
106
115
  --config <path> can target a config file for tests or scripted setup.
107
116
 
108
117
  Examples:
118
+ yeaft-agent llm setup
119
+ yeaft-agent llm use github-copilot --model claude-sonnet-4.5 --fast gpt-4.1
109
120
  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
121
  yeaft-agent llm set-model --primary openai/gpt-5 --fast openai/gpt-4.1
112
122
  yeaft-agent llm show --reveal
113
123
  `);
114
124
  }
115
125
 
116
- function handleLlmCommand(args) {
126
+ async function handleLlmCommand(args) {
117
127
  const subcommand = args[0];
118
128
 
119
129
  try {
120
- const options = parseLlmArgs(args.slice(1));
130
+ const options = parseLlmArgs(args.slice(subcommand === 'use' ? 2 : 1));
121
131
  const configPath = options.config || getDefaultYeaftConfigPath();
122
132
  if (!subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help') {
123
133
  printLlmHelp();
@@ -132,6 +142,33 @@ function handleLlmCommand(args) {
132
142
 
133
143
  const current = readLocalLlmConfig(configPath);
134
144
  let result;
145
+ if (subcommand === 'setup') {
146
+ await runLlmSetup(current, configPath);
147
+ return;
148
+ }
149
+
150
+ if (subcommand === 'use') {
151
+ const preset = args[1];
152
+ if (preset === 'github-copilot') {
153
+ result = await useGitHubCopilot(current, options);
154
+ writeLocalLlmConfig(result.config, configPath);
155
+ console.log(`Configured GitHub Copilot provider with ${result.discovery.models.length} ${result.discovery.source} models.`);
156
+ if (result.discovery.warning) console.log(`Warning: ${result.discovery.warning}`);
157
+ console.log(`Primary model: ${result.config.primaryModel}`);
158
+ if (result.config.fastModel) console.log(`Fast model: ${result.config.fastModel}`);
159
+ return;
160
+ }
161
+ if (preset === 'openai-compatible') {
162
+ result = await useOpenAICompatible(current, options, process.env);
163
+ writeLocalLlmConfig(result.config, configPath);
164
+ console.log(`Configured ${result.provider.name} with ${result.discovery.models.length} live models.`);
165
+ console.log(`Primary model: ${result.config.primaryModel}`);
166
+ if (result.config.fastModel) console.log(`Fast model: ${result.config.fastModel}`);
167
+ return;
168
+ }
169
+ throw new Error(`Unsupported llm use preset: ${preset || '(missing)'}`);
170
+ }
171
+
135
172
  if (subcommand === 'add-provider') {
136
173
  result = addOrUpdateProvider(current, options, process.env);
137
174
  writeLocalLlmConfig(result.config, configPath);
@@ -167,12 +204,50 @@ function handleLlmCommand(args) {
167
204
  }
168
205
  }
169
206
 
207
+
208
+ async function runLlmSetup(current, configPath) {
209
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
210
+ throw new Error('Interactive setup requires a TTY. Use `yeaft-agent llm use github-copilot --model <modelId>` in scripts.');
211
+ }
212
+
213
+ const rl = createInterface({ input, output });
214
+ try {
215
+ console.log('Yeaft LLM setup');
216
+ console.log('1) GitHub Copilot (uses local device token / gh auth, no API key in config)');
217
+ console.log('2) Advanced manual provider (use add-provider command)');
218
+ const choice = (await rl.question('Choose provider [1]: ')).trim() || '1';
219
+ if (choice !== '1') {
220
+ console.log('Use `yeaft-agent llm add-provider --help` for advanced endpoints.');
221
+ return;
222
+ }
223
+
224
+ const discovery = await useGitHubCopilot(current, { model: '__placeholder__', allowUnknownModel: true });
225
+ const ids = discovery.discovery.models;
226
+ console.log('\nAvailable GitHub Copilot models:');
227
+ ids.forEach((id, idx) => console.log(` ${idx + 1}) ${id}`));
228
+ const answer = (await rl.question('Primary model number or id: ')).trim();
229
+ const primary = ids[Number(answer) - 1] || answer;
230
+ if (!primary) throw new Error('A primary model is required.');
231
+ const fastAnswer = (await rl.question('Fast model number or id (optional): ')).trim();
232
+ const fast = fastAnswer ? (ids[Number(fastAnswer) - 1] || fastAnswer) : null;
233
+ const result = await useGitHubCopilot(current, { model: primary, fast, allowUnknownModel: false });
234
+ writeLocalLlmConfig(result.config, configPath);
235
+ console.log(`Configured GitHub Copilot with ${result.discovery.models.length} ${result.discovery.source} models.`);
236
+ if (result.discovery.warning) console.log(`Warning: ${result.discovery.warning}`);
237
+ console.log(`Primary model: ${result.config.primaryModel}`);
238
+ if (result.config.fastModel) console.log(`Fast model: ${result.config.fastModel}`);
239
+ } finally {
240
+ rl.close();
241
+ }
242
+ }
243
+
170
244
  function parseLlmArgs(args) {
171
245
  const options = {};
172
246
  for (let i = 0; i < args.length; i++) {
173
247
  const arg = args[i];
174
- if (arg === '--reveal') {
175
- options.reveal = true;
248
+ if (arg === '--reveal' || arg === '--allow-unknown-model') {
249
+ const key = arg === '--reveal' ? 'reveal' : 'allowUnknownModel';
250
+ options[key] = true;
176
251
  continue;
177
252
  }
178
253
  const key = arg.startsWith('--') ? arg.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase()) : null;
@@ -36,6 +36,7 @@ import { sendToServer, flushMessageBuffer } from './buffer.js';
36
36
  import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
37
37
  import { loadMcpServers, updateMcpConfig } from '../mcp.js';
38
38
  import { getLlmConfig, updateLlmConfig, getYeaftSettings, updateYeaftSettings, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../yeaft/config-api.js';
39
+ import { discoverLlmModels } from '../llm-model-discovery.js';
39
40
  import { fetchModelsDev } from '../yeaft/llm/models-dev.js';
40
41
  import { handleYeaftSessionSend, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, handleYeaftLoadHistory, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftCreateSession, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, handleYeaftMcpList, handleYeaftMcpAdd, handleYeaftMcpRemove, handleYeaftMcpReload, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager } from '../yeaft/web-bridge.js';
41
42
  import { startYeaftStatusRefresh, refreshYeaftStatus } from '../yeaft/status-cache.js';
@@ -351,6 +352,28 @@ export async function handleMessage(msg) {
351
352
  break;
352
353
  }
353
354
 
355
+ case 'discover_llm_models': {
356
+ try {
357
+ const result = await discoverLlmModels(msg || {});
358
+ sendToServer({
359
+ type: 'llm_models_discovered',
360
+ agentId: msg.agentId,
361
+ requestId: msg.requestId,
362
+ providerType: msg.providerType || msg.provider || msg.preset,
363
+ ...result,
364
+ });
365
+ } catch (e) {
366
+ sendToServer({
367
+ type: 'llm_models_discovered',
368
+ agentId: msg.agentId,
369
+ requestId: msg.requestId,
370
+ providerType: msg.providerType || msg.provider || msg.preset,
371
+ error: e.message || String(e),
372
+ });
373
+ }
374
+ break;
375
+ }
376
+
354
377
  case 'update_llm_config': {
355
378
  // Capture the user's intent BEFORE updateLlmConfig — the return
356
379
  // envelope ALWAYS populates `language` (falls back to 'en'), so
package/llm-config-cli.js CHANGED
@@ -1,6 +1,12 @@
1
1
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
2
  import { dirname, join } from 'path';
3
3
  import { homedir } from 'os';
4
+ import {
5
+ discoverGitHubCopilotModels,
6
+ discoverOpenAICompatibleModels,
7
+ GITHUB_COPILOT_PROVIDER,
8
+ modelIdsFromProviderModels,
9
+ } from './llm-model-discovery.js';
4
10
 
5
11
  const VALID_PROTOCOLS = new Set(['anthropic', 'openai-responses']);
6
12
  const VALID_CREDENTIAL_PROVIDERS = new Set(['github-copilot']);
@@ -194,3 +200,86 @@ function validateCredentials(options, env) {
194
200
  }
195
201
  }
196
202
  }
203
+
204
+
205
+ export async function useGitHubCopilot(config, options = {}) {
206
+ const primaryModel = requireNonEmpty(options.model, '--model');
207
+ const fastModel = options.fast ? String(options.fast).trim() : null;
208
+ const discovery = await discoverGitHubCopilotModels(options);
209
+ const discoveredIds = modelIdsFromProviderModels(discovery.providerModels);
210
+ const allowUnknown = Boolean(options.allowUnknownModel);
211
+
212
+ for (const model of [primaryModel, fastModel].filter(Boolean)) {
213
+ if (!allowUnknown && !discoveredIds.includes(model)) {
214
+ throw new Error(`Model "${model}" was not found in the GitHub Copilot model catalog. Use --allow-unknown-model to save it anyway.`);
215
+ }
216
+ }
217
+
218
+ const providerModels = [...discovery.providerModels];
219
+ if (allowUnknown) {
220
+ const known = new Set(discoveredIds);
221
+ for (const model of [primaryModel, fastModel].filter(Boolean)) {
222
+ if (!known.has(model)) {
223
+ providerModels.push(model);
224
+ known.add(model);
225
+ }
226
+ }
227
+ }
228
+
229
+ const next = { ...config };
230
+ const providers = Array.isArray(config.providers) ? [...config.providers] : [];
231
+ const provider = {
232
+ ...GITHUB_COPILOT_PROVIDER,
233
+ models: providerModels,
234
+ };
235
+ const index = providers.findIndex(p => p && p.name === provider.name);
236
+ if (index >= 0) providers[index] = provider;
237
+ else providers.push(provider);
238
+
239
+ next.providers = providers;
240
+ next.primaryModel = `${provider.name}/${primaryModel}`;
241
+ if (fastModel) next.fastModel = `${provider.name}/${fastModel}`;
242
+
243
+ return { config: next, provider, discovery };
244
+ }
245
+
246
+
247
+ export async function useOpenAICompatible(config, options = {}, env = process.env) {
248
+ const name = options.name ? String(options.name).trim() : 'openai';
249
+ const baseUrl = requireNonEmpty(options.baseUrl, '--base-url');
250
+ const primaryModel = requireNonEmpty(options.model, '--model');
251
+ const fastModel = options.fast ? String(options.fast).trim() : null;
252
+ validateCredentials(options, env);
253
+ const apiKey = options.apiKey || env[options.apiKeyEnv];
254
+ const discovery = await discoverOpenAICompatibleModels({ ...options, apiKey });
255
+ const discoveredIds = modelIdsFromProviderModels(discovery.providerModels);
256
+ const allowUnknown = Boolean(options.allowUnknownModel);
257
+
258
+ for (const model of [primaryModel, fastModel].filter(Boolean)) {
259
+ if (!allowUnknown && !discoveredIds.includes(model)) {
260
+ throw new Error(`Model "${model}" was not found in the provider model catalog. Use --allow-unknown-model to save it anyway.`);
261
+ }
262
+ }
263
+
264
+ const providerModels = [...discovery.providerModels];
265
+ if (allowUnknown) {
266
+ const known = new Set(discoveredIds);
267
+ for (const model of [primaryModel, fastModel].filter(Boolean)) {
268
+ if (!known.has(model)) {
269
+ providerModels.push(model);
270
+ known.add(model);
271
+ }
272
+ }
273
+ }
274
+
275
+ const next = { ...config };
276
+ const providers = Array.isArray(config.providers) ? [...config.providers] : [];
277
+ const provider = { name, baseUrl, apiKey, protocol: 'openai-responses', models: providerModels };
278
+ const index = providers.findIndex(p => p && p.name === provider.name);
279
+ if (index >= 0) providers[index] = provider;
280
+ else providers.push(provider);
281
+ next.providers = providers;
282
+ next.primaryModel = `${provider.name}/${primaryModel}`;
283
+ if (fastModel) next.fastModel = `${provider.name}/${fastModel}`;
284
+ return { config: next, provider, discovery };
285
+ }
@@ -0,0 +1,169 @@
1
+ import { getApiToken, copilotRequestHeaders } from './yeaft/llm/credentials/github-copilot.js';
2
+
3
+ export const GITHUB_COPILOT_PROVIDER = {
4
+ name: 'github-copilot',
5
+ baseUrl: 'https://api.githubcopilot.com',
6
+ credentialProvider: 'github-copilot',
7
+ protocol: 'openai-responses',
8
+ };
9
+
10
+ export const FALLBACK_GITHUB_COPILOT_MODELS = [
11
+ 'gpt-5.4',
12
+ 'gpt-5.4-mini',
13
+ 'gpt-5-mini',
14
+ 'gpt-5.3-codex',
15
+ 'gpt-5.2-codex',
16
+ 'gpt-4.1',
17
+ 'gpt-4o',
18
+ 'gpt-4o-mini',
19
+ 'claude-opus-4.6',
20
+ 'claude-sonnet-4.6',
21
+ 'claude-sonnet-4.5',
22
+ 'claude-haiku-4.5',
23
+ 'gemini-2.5-pro',
24
+ ];
25
+
26
+ function modelId(item) {
27
+ if (typeof item === 'string') return item.trim();
28
+ if (item && typeof item === 'object') return String(item.id || '').trim();
29
+ return '';
30
+ }
31
+
32
+ function uniqueIds(items) {
33
+ const seen = new Set();
34
+ const ids = [];
35
+ for (const item of Array.isArray(items) ? items : []) {
36
+ const id = modelId(item);
37
+ if (!id || seen.has(id)) continue;
38
+ seen.add(id);
39
+ ids.push(id);
40
+ }
41
+ return ids;
42
+ }
43
+
44
+ export function modelEntryForProvider(id) {
45
+ const value = String(id || '').trim();
46
+ if (!value) return null;
47
+ return value.toLowerCase().startsWith('claude-')
48
+ ? { id: value, protocol: 'anthropic' }
49
+ : value;
50
+ }
51
+
52
+ export function modelIdsFromProviderModels(models) {
53
+ return uniqueIds(models);
54
+ }
55
+
56
+ export function providerModelsFromIds(ids) {
57
+ return uniqueIds(ids).map(modelEntryForProvider).filter(Boolean);
58
+ }
59
+
60
+ function parseModelPayload(payload) {
61
+ if (Array.isArray(payload)) return uniqueIds(payload);
62
+ if (payload && Array.isArray(payload.data)) return uniqueIds(payload.data);
63
+ if (payload && Array.isArray(payload.models)) return uniqueIds(payload.models);
64
+ return [];
65
+ }
66
+
67
+ async function readJson(res) {
68
+ const text = await res.text();
69
+ if (!text.trim()) return null;
70
+ try {
71
+ return JSON.parse(text);
72
+ } catch (e) {
73
+ throw new Error(`Invalid JSON model catalog: ${e.message}`);
74
+ }
75
+ }
76
+
77
+ function fallbackResult(provider, reason) {
78
+ return {
79
+ provider,
80
+ models: [...FALLBACK_GITHUB_COPILOT_MODELS],
81
+ providerModels: providerModelsFromIds(FALLBACK_GITHUB_COPILOT_MODELS),
82
+ source: 'fallback',
83
+ warning: `Live model catalog unavailable (${reason}); using fallback Copilot model list.`,
84
+ };
85
+ }
86
+
87
+ export async function discoverGitHubCopilotModels({ fetchFn = fetch, getTokenFn = getApiToken } = {}) {
88
+ const tokenInfo = await getTokenFn({ fetchFn });
89
+ if (!tokenInfo?.token) {
90
+ const err = new Error('GitHub Copilot credential not found. Run `gh auth login` or complete the Copilot device login first.');
91
+ err.code = 'COPILOT_CREDENTIAL_MISSING';
92
+ throw err;
93
+ }
94
+
95
+ try {
96
+ const res = await fetchFn('https://api.githubcopilot.com/models', {
97
+ method: 'GET',
98
+ headers: {
99
+ Authorization: `Bearer ${tokenInfo.token}`,
100
+ Accept: 'application/json',
101
+ ...copilotRequestHeaders({ isAgentTurn: true }),
102
+ },
103
+ });
104
+ if (res.status === 401 || res.status === 403) {
105
+ const err = new Error('GitHub Copilot credential is invalid or lacks Copilot access. Re-authenticate with `gh auth login` or complete the Copilot device login again.');
106
+ err.code = 'COPILOT_AUTH_INVALID';
107
+ throw err;
108
+ }
109
+ if (!res.ok) return fallbackResult(GITHUB_COPILOT_PROVIDER, `HTTP ${res.status}`);
110
+ const payload = await readJson(res);
111
+ const models = parseModelPayload(payload);
112
+ if (models.length === 0) return fallbackResult(GITHUB_COPILOT_PROVIDER, 'empty catalog');
113
+ return {
114
+ provider: GITHUB_COPILOT_PROVIDER,
115
+ models,
116
+ providerModels: providerModelsFromIds(models),
117
+ source: 'live',
118
+ warning: null,
119
+ };
120
+ } catch (e) {
121
+ if (e?.code === 'COPILOT_AUTH_INVALID') throw e;
122
+ return fallbackResult(GITHUB_COPILOT_PROVIDER, e.message || String(e));
123
+ }
124
+ }
125
+
126
+ export function openAIModelsUrl(baseUrl) {
127
+ const raw = String(baseUrl || '').trim();
128
+ if (!raw) throw new Error('OpenAI-compatible discovery requires a base URL.');
129
+ const url = new URL(raw);
130
+ const pathname = url.pathname.replace(/\/+$/, '');
131
+ if (pathname.endsWith('/models')) {
132
+ url.pathname = pathname;
133
+ } else if (pathname.endsWith('/v1')) {
134
+ url.pathname = `${pathname}/models`;
135
+ } else {
136
+ url.pathname = `${pathname}/v1/models`;
137
+ }
138
+ return url.toString();
139
+ }
140
+
141
+ export async function discoverOpenAICompatibleModels({ baseUrl, apiKey, fetchFn = fetch } = {}) {
142
+ if (!apiKey) throw new Error('OpenAI-compatible discovery requires an API key.');
143
+ const url = openAIModelsUrl(baseUrl);
144
+ const res = await fetchFn(url, {
145
+ method: 'GET',
146
+ headers: {
147
+ Authorization: `Bearer ${apiKey}`,
148
+ Accept: 'application/json',
149
+ },
150
+ });
151
+ if (!res.ok) throw new Error(`Model discovery failed: HTTP ${res.status}`);
152
+ const payload = await readJson(res);
153
+ const models = parseModelPayload(payload);
154
+ if (models.length === 0) throw new Error('Model discovery returned no models.');
155
+ return {
156
+ provider: { baseUrl, protocol: 'openai-responses' },
157
+ models,
158
+ providerModels: providerModelsFromIds(models),
159
+ source: 'live',
160
+ warning: null,
161
+ };
162
+ }
163
+
164
+ export async function discoverLlmModels(options = {}) {
165
+ const providerType = options.providerType || options.provider || options.preset;
166
+ if (providerType === 'github-copilot') return discoverGitHubCopilotModels(options);
167
+ if (providerType === 'openai-compatible') return discoverOpenAICompatibleModels(options);
168
+ throw new Error(`Unsupported provider preset: ${providerType || '(missing)'}`);
169
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.946",
3
+ "version": "0.1.948",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",