@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 +81 -6
- package/connection/buffer.js +5 -1
- package/connection/index.js +11 -1
- package/connection/message-router.js +32 -0
- package/context.js +5 -0
- package/index.js +5 -1
- 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;
|
package/connection/buffer.js
CHANGED
|
@@ -41,7 +41,11 @@ export async function sendToServer(msg) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
try {
|
|
44
|
-
|
|
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 {
|
package/connection/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|