@yeaft/webchat-agent 0.1.864 → 0.1.865

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.
@@ -18,7 +18,7 @@ import {
18
18
  handleGitStatus, handleGitDiff, handleGitAdd, handleGitReset, handleGitRestore, handleGitCommit, handleGitPush,
19
19
  handleFileSearch, handleCreateFile, handleDeleteFiles, handleMoveFiles, handleCopyFiles, handleUploadToDir, handleTransferFiles
20
20
  } from '../workbench.js';
21
- import { handleListHistorySessions, handleListFolders } from '../history.js';
21
+ import { handleListHistorySessions, handleListFolders, handleListModels } from '../history.js';
22
22
  import {
23
23
  createConversation, resumeConversation, deleteConversation,
24
24
  handleRefreshConversation, handleCancelExecution,
@@ -112,6 +112,10 @@ export async function handleMessage(msg) {
112
112
  await handleListFolders(msg);
113
113
  break;
114
114
 
115
+ case 'list_models':
116
+ await handleListModels(msg);
117
+ break;
118
+
115
119
  case 'transfer_files':
116
120
  await handleTransferFiles(msg);
117
121
  break;
package/history.js CHANGED
@@ -252,3 +252,32 @@ export async function handleListFolders(msg) {
252
252
  });
253
253
  }
254
254
  }
255
+
256
+ // 列出指定 provider 下可选 model
257
+ export async function handleListModels(msg) {
258
+ const { requestId, _requestClientId, provider } = msg;
259
+ const providerName = provider || DEFAULT_PROVIDER;
260
+ try {
261
+ const driver = getProvider(providerName);
262
+ let models = [];
263
+ if (typeof driver.listModels === 'function') {
264
+ models = await driver.listModels();
265
+ }
266
+ ctx.sendToServer({
267
+ type: 'models_list',
268
+ requestId,
269
+ _requestClientId,
270
+ provider: providerName,
271
+ models: Array.isArray(models) ? models : [],
272
+ });
273
+ } catch (e) {
274
+ ctx.sendToServer({
275
+ type: 'models_list',
276
+ requestId,
277
+ _requestClientId,
278
+ provider: providerName,
279
+ models: [],
280
+ error: e.message,
281
+ });
282
+ }
283
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.864",
3
+ "version": "0.1.865",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,17 +1,131 @@
1
1
  /**
2
- * Hard-coded known Copilot CLI model IDs. The CLI accepts `--model <id>` but
3
- * doesn't expose a list over ACP today, so we ship a curated set that maps
4
- * to what `copilot /model` shows in interactive mode. Add to this list as
5
- * Copilot publishes new models — no other code change required.
2
+ * Copilot model list.
3
+ *
4
+ * The Copilot CLI accepts `--model <id>` and the agent-facing API exposes
5
+ * `GET https://api.githubcopilot.com/models`the same endpoint VS Code's
6
+ * model picker uses. We hit it on demand (cached in-process for 10 min),
7
+ * filter to picker-enabled chat models, and fall back to a curated static
8
+ * list if the network call fails (so the UI never shows an empty picker).
9
+ *
10
+ * Auth re-uses the existing `agent/yeaft/llm/credentials/github-copilot.js`
11
+ * credential pipeline — gh CLI / env / persisted OAuth all work; the user
12
+ * does not need to paste an API key as long as Copilot CLI is logged in.
6
13
  */
7
- export const COPILOT_MODELS = Object.freeze([
8
- { id: 'gpt-5', label: 'GPT-5' },
9
- { id: 'gpt-5-mini', label: 'GPT-5 Mini' },
10
- { id: 'claude-sonnet-4', label: 'Claude Sonnet 4' },
11
- { id: 'claude-sonnet-4.5', label: 'Claude Sonnet 4.5' },
12
- { id: 'claude-opus-4', label: 'Claude Opus 4' },
13
- { id: 'gpt-4o', label: 'GPT-4o' },
14
- { id: 'o1', label: 'o1' },
14
+
15
+ import { getApiToken, copilotRequestHeaders, validateRawToken, resolveRawToken } from '../yeaft/llm/credentials/github-copilot.js';
16
+ import { readFile } from 'fs/promises';
17
+ import { homedir } from 'os';
18
+ import { join } from 'path';
19
+
20
+ // Curated fallback — small, but covers the common picks if /models is down or
21
+ // the user has no Copilot auth available yet.
22
+ export const FALLBACK_COPILOT_MODELS = Object.freeze([
23
+ { id: 'claude-sonnet-4.5', label: 'Claude Sonnet 4.5', vendor: 'Anthropic' },
24
+ { id: 'claude-sonnet-4', label: 'Claude Sonnet 4', vendor: 'Anthropic' },
25
+ { id: 'claude-opus-4.1', label: 'Claude Opus 4.1', vendor: 'Anthropic' },
26
+ { id: 'gpt-5', label: 'GPT-5', vendor: 'OpenAI' },
27
+ { id: 'gpt-5-mini', label: 'GPT-5 Mini', vendor: 'OpenAI' },
28
+ { id: 'gpt-4.1', label: 'GPT-4.1', vendor: 'OpenAI' },
29
+ { id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', vendor: 'Google' },
15
30
  ]);
16
31
 
17
32
  export const DEFAULT_COPILOT_MODEL = 'claude-sonnet-4.5';
33
+
34
+ const MODELS_ENDPOINT = 'https://api.githubcopilot.com/models';
35
+ const CACHE_TTL_MS = 10 * 60 * 1000;
36
+ const COPILOT_CLI_CONFIG = join(homedir(), '.copilot', 'config.json');
37
+
38
+ /**
39
+ * Last-resort token source: the Copilot CLI itself caches an OAuth token at
40
+ * ~/.copilot/config.json after `copilot login`. If the standard yeaft
41
+ * credential pipeline (env / gh CLI / persisted device flow) didn't surface
42
+ * a token, we re-use the CLI's. Same auth surface — no extra perm needed.
43
+ */
44
+ async function _resolveCopilotCliToken() {
45
+ try {
46
+ const raw = await readFile(COPILOT_CLI_CONFIG, 'utf8');
47
+ // Copilot CLI's config.json starts with `//` comments. Strip line comments
48
+ // before JSON.parse — the file otherwise has no string-context `//`.
49
+ const cleaned = raw.split('\n').filter(l => !l.trim().startsWith('//')).join('\n');
50
+ const cfg = JSON.parse(cleaned);
51
+ const tokens = cfg?.copilotTokens && typeof cfg.copilotTokens === 'object' ? cfg.copilotTokens : null;
52
+ if (!tokens) return null;
53
+ for (const tok of Object.values(tokens)) {
54
+ if (typeof tok === 'string' && validateRawToken(tok).valid) return tok;
55
+ }
56
+ return null;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ async function _resolveBearerToken() {
63
+ // The /models endpoint accepts the GitHub OAuth token directly as a Bearer
64
+ // — no token exchange needed (unlike the chat-completion endpoints). So we
65
+ // prefer the raw OAuth from the standard yeaft pipeline first, then fall
66
+ // back to the Copilot CLI's own cached OAuth at ~/.copilot/config.json.
67
+ try {
68
+ const raw = await resolveRawToken();
69
+ if (raw?.token) return raw.token;
70
+ } catch { /* fall through */ }
71
+ const cliRaw = await _resolveCopilotCliToken();
72
+ if (cliRaw) return cliRaw;
73
+ // Last resort: try exchanged token (works for chat endpoints; may also work
74
+ // if /models gained that auth path in future).
75
+ const cred = await getApiToken();
76
+ return cred?.token || null;
77
+ }
78
+
79
+ let _cache = null; // { models, fetchedAt }
80
+ let _inflight = null; // dedupes concurrent cold-cache calls
81
+
82
+ /**
83
+ * Returns a list of `{id, label, vendor, preview, family}` records,
84
+ * picker-enabled chat models only. Cached for 10 minutes. Never throws —
85
+ * falls back to the static list on any error (including network timeout).
86
+ */
87
+ export async function listCopilotModels({ force = false } = {}) {
88
+ if (!force && _cache && Date.now() - _cache.fetchedAt < CACHE_TTL_MS) {
89
+ return _cache.models.slice();
90
+ }
91
+ if (_inflight) return (await _inflight).slice();
92
+ _inflight = (async () => {
93
+ const ac = new AbortController();
94
+ const timer = setTimeout(() => ac.abort(), 5000);
95
+ try {
96
+ const token = await _resolveBearerToken();
97
+ if (!token) return FALLBACK_COPILOT_MODELS.slice();
98
+ const res = await fetch(MODELS_ENDPOINT, {
99
+ signal: ac.signal,
100
+ headers: { ...copilotRequestHeaders({ isAgentTurn: false }), Authorization: `Bearer ${token}` },
101
+ });
102
+ if (!res.ok) return FALLBACK_COPILOT_MODELS.slice();
103
+ const body = await res.json();
104
+ const data = Array.isArray(body?.data) ? body.data : [];
105
+ const models = data
106
+ .filter(m => m && m.model_picker_enabled && m.capabilities?.type === 'chat')
107
+ .map(m => ({
108
+ id: m.id,
109
+ label: m.name || m.id,
110
+ vendor: m.vendor || '',
111
+ preview: !!m.preview,
112
+ family: m.capabilities?.family || '',
113
+ }));
114
+ if (!models.length) return FALLBACK_COPILOT_MODELS.slice();
115
+ _cache = { models, fetchedAt: Date.now() };
116
+ return models.slice();
117
+ } catch {
118
+ return FALLBACK_COPILOT_MODELS.slice();
119
+ } finally {
120
+ clearTimeout(timer);
121
+ }
122
+ })();
123
+ try {
124
+ return (await _inflight).slice();
125
+ } finally {
126
+ _inflight = null;
127
+ }
128
+ }
129
+
130
+ /** Tests only. */
131
+ export function _resetCopilotModelsCacheForTests() { _cache = null; _inflight = null; }
@@ -6,7 +6,7 @@ import { join } from 'path';
6
6
  import { DatabaseSync } from 'node:sqlite';
7
7
  import ctx from '../context.js';
8
8
  import { AcpClient } from './acp-client.js';
9
- import { COPILOT_MODELS, DEFAULT_COPILOT_MODEL } from './copilot-models.js';
9
+ import { listCopilotModels, DEFAULT_COPILOT_MODEL } from './copilot-models.js';
10
10
 
11
11
  export const name = 'copilot';
12
12
 
@@ -582,9 +582,12 @@ function _knownCopilotTools() {
582
582
  ];
583
583
  }
584
584
 
585
- /** Exported for the model picker UI. */
586
- export function listModels() {
587
- return COPILOT_MODELS.slice();
585
+ /**
586
+ * Exported for the model picker UI. Async — hits Copilot's /models endpoint
587
+ * (cached) and falls back to a static list if auth/network fails.
588
+ */
589
+ export async function listModels() {
590
+ return await listCopilotModels();
588
591
  }
589
592
 
590
593
  export default { name, capabilities, start, sendInput, abort, clear, listFolders, listSessions, loadHistory, listModels, respondToPermissionRequest };