@yeaft/webchat-agent 0.1.945 → 0.1.947

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;
@@ -41,7 +41,11 @@ export async function sendToServer(msg) {
41
41
  }
42
42
 
43
43
  try {
44
- if (ctx.sessionKey) {
44
+ // feat-ws-plaintext-negotiation: encrypt only when the server has
45
+ // NOT advertised plaintext acceptance. Defaults to encrypted for
46
+ // back-compat with old servers; flipped to plaintext when the
47
+ // `registered` frame includes `acceptPlaintext: true`.
48
+ if (ctx.serverEncryptionRequired && ctx.sessionKey) {
45
49
  const encrypted = await encrypt(msg, ctx.sessionKey);
46
50
  ctx.ws.send(JSON.stringify(encrypted));
47
51
  } else {
@@ -21,7 +21,17 @@ export function connect() {
21
21
  console.log(`Disallowed tools: ${ctx.CONFIG.disallowedTools.join(', ')}`);
22
22
  }
23
23
 
24
- ctx.ws = new WebSocket(url);
24
+ ctx.ws = new WebSocket(url, {
25
+ // Match server's permessage-deflate config (bounded memory,
26
+ // skip compression for small frames). The `ws` library handles
27
+ // streaming compression so we no longer need the synchronous
28
+ // gzip-before-encrypt on the hot path.
29
+ perMessageDeflate: {
30
+ clientNoContextTakeover: true,
31
+ serverNoContextTakeover: true,
32
+ threshold: 1024
33
+ }
34
+ });
25
35
 
26
36
  ctx.ws.on('open', () => {
27
37
  console.log('Connected to server, waiting for auth challenge...');
@@ -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';
@@ -48,6 +49,15 @@ export async function handleMessage(msg) {
48
49
  console.log('Encryption enabled');
49
50
  }
50
51
 
52
+ // feat-ws-plaintext-negotiation: new server advertises that it
53
+ // will accept plaintext frames from us. Stop encrypting outbound.
54
+ // The receive path (parseMessage) stays unconditional so the old
55
+ // ciphertext that may already be in flight still decrypts.
56
+ if (msg.acceptPlaintext === true) {
57
+ ctx.serverEncryptionRequired = false;
58
+ console.log('[WS] Server accepts plaintext, disabling outbound encryption');
59
+ }
60
+
51
61
  // 只保存基本配置(不再保存 agentId,因为现在用 agentName 作为 ID)
52
62
  ctx.saveConfig({
53
63
  serverUrl: ctx.CONFIG.serverUrl,
@@ -342,6 +352,28 @@ export async function handleMessage(msg) {
342
352
  break;
343
353
  }
344
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
+
345
377
  case 'update_llm_config': {
346
378
  // Capture the user's intent BEFORE updateLlmConfig — the return
347
379
  // envelope ALWAYS populates `language` (falls back to 'en'), so
package/context.js CHANGED
@@ -4,6 +4,11 @@
4
4
  export default {
5
5
  ws: null,
6
6
  sessionKey: null,
7
+ // feat-ws-plaintext-negotiation: defaults `true` (= assume old server,
8
+ // keep encrypting outbound for back-compat). Cleared to `false` when
9
+ // the agent receives `registered { acceptPlaintext: true }` from a
10
+ // new server — see message-router.js `case 'registered'`.
11
+ serverEncryptionRequired: true,
7
12
  conversations: new Map(),
8
13
  terminals: new Map(),
9
14
  proxyPorts: [],
package/index.js CHANGED
@@ -111,7 +111,11 @@ loadMcpServers();
111
111
 
112
112
  // Agent capabilities(启动时自动检测)
113
113
  async function detectCapabilities() {
114
- const capabilities = ['background_tasks', 'file_editor', 'ping_session'];
114
+ // feat-ws-plaintext-negotiation: `plaintext-ok` advertises that this
115
+ // agent build can speak plaintext WS frames. New servers see this and
116
+ // flip `agent.encryptOutbound = false`, stopping outbound encryption
117
+ // to this peer. Old servers ignore the unknown capability token.
118
+ const capabilities = ['background_tasks', 'file_editor', 'ping_session', 'plaintext-ok'];
115
119
  const pty = await loadNodePty();
116
120
  if (pty) capabilities.push('terminal');
117
121
 
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.945",
3
+ "version": "0.1.947",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",