@yeaft/webchat-agent 0.1.946 → 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 +81 -6
- package/connection/message-router.js +23 -0
- package/llm-config-cli.js +89 -0
- package/llm-model-discovery.js +169 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
+
}
|