@melaya/runner 1.1.26 → 1.1.28
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/dist/assistantHost.py +46 -3
- package/dist/cli.js +22 -12
- package/dist/connection.js +2 -2
- package/dist/copilotLogin.d.ts +1 -0
- package/dist/copilotLogin.js +90 -0
- package/dist/detect.d.ts +1 -1
- package/dist/detect.js +91 -0
- package/package.json +1 -1
package/dist/assistantHost.py
CHANGED
|
@@ -426,6 +426,39 @@ def _log(msg: str) -> None:
|
|
|
426
426
|
pass
|
|
427
427
|
|
|
428
428
|
|
|
429
|
+
# Failures the user can act on, told apart from the ones only we can. The runner
|
|
430
|
+
# already writes a full traceback to stderr; this builds the ONE line that reaches
|
|
431
|
+
# the panel, so it has to carry the exception type when the message is empty and
|
|
432
|
+
# stay readable when it is not.
|
|
433
|
+
_FRIENDLY_TURN_ERRORS = (
|
|
434
|
+
("timeout", "The model did not respond in time. Try again, or pick a different model."),
|
|
435
|
+
("timed out", "The model did not respond in time. Try again, or pick a different model."),
|
|
436
|
+
("connection", "Could not reach the model provider from this machine. Check the network and try again."),
|
|
437
|
+
("resolve", "Could not reach the model provider from this machine. Check the network and try again."),
|
|
438
|
+
("refused", "The model provider refused the connection from this machine."),
|
|
439
|
+
("ssl", "The connection to the model provider could not be secured on this machine."),
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _turn_error_message(exc: BaseException) -> str:
|
|
444
|
+
"""One line for the panel. Falls back to the exception TYPE, never to nothing."""
|
|
445
|
+
raw = str(exc).strip()
|
|
446
|
+
name = type(exc).__name__
|
|
447
|
+
probe = (raw or name).lower()
|
|
448
|
+
for needle, friendly in _FRIENDLY_TURN_ERRORS:
|
|
449
|
+
if needle in probe:
|
|
450
|
+
detail = ": " + raw[:200] if raw else ""
|
|
451
|
+
return friendly + " (" + name + detail + ")"
|
|
452
|
+
if raw:
|
|
453
|
+
return raw[:500]
|
|
454
|
+
# Empty message: the type is all there is, and it is genuinely useful -
|
|
455
|
+
# TimeoutError and ConnectionResetError point at different fixes.
|
|
456
|
+
return (
|
|
457
|
+
"The assistant turn failed with " + name + " and no further detail. "
|
|
458
|
+
"The full traceback is in the runner log."
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
429
462
|
def _extract_text(result) -> str:
|
|
430
463
|
"""Pull the final assistant text out of an agentscope reply Msg."""
|
|
431
464
|
if result is None:
|
|
@@ -907,7 +940,14 @@ def _run_turn(agent, turn_id: str, message: str, browser_turn: bool = False) ->
|
|
|
907
940
|
except Exception as exc:
|
|
908
941
|
_log("turn error:\n" + traceback.format_exc())
|
|
909
942
|
_stream["turnId"] = ""
|
|
910
|
-
|
|
943
|
+
# NEVER surface a bare "assistant turn failed". That fallback fired only
|
|
944
|
+
# when str(exc) was EMPTY - exactly the case where the exception TYPE is
|
|
945
|
+
# the sole remaining clue, and it was being discarded. Several of the
|
|
946
|
+
# likeliest failures here stringify to nothing at all (asyncio.TimeoutError,
|
|
947
|
+
# httpx.ReadTimeout, ConnectionResetError), so the panel showed a message
|
|
948
|
+
# that named neither what broke nor where. Reported 2026-09-03: two turns
|
|
949
|
+
# in a row with no cause anywhere on screen.
|
|
950
|
+
_emit(turn_id, "error", message=_turn_error_message(exc))
|
|
911
951
|
_emit(turn_id, "done")
|
|
912
952
|
return
|
|
913
953
|
|
|
@@ -921,7 +961,10 @@ def _run_turn(agent, turn_id: str, message: str, browser_turn: bool = False) ->
|
|
|
921
961
|
if result is _WALLCLOCK:
|
|
922
962
|
_stream["turnId"] = ""
|
|
923
963
|
_log("browser turn wall-clock budget exceeded")
|
|
924
|
-
_emit(turn_id, "error", message=
|
|
964
|
+
_emit(turn_id, "error", message=(
|
|
965
|
+
"This browser task ran past its time budget and was stopped. "
|
|
966
|
+
"Ask again with a narrower step, or split it into two."
|
|
967
|
+
))
|
|
925
968
|
_emit(turn_id, "done")
|
|
926
969
|
return
|
|
927
970
|
|
|
@@ -988,7 +1031,7 @@ def main() -> int:
|
|
|
988
1031
|
agent = _build_agent()
|
|
989
1032
|
except Exception as exc:
|
|
990
1033
|
_log("boot failed:\n" + traceback.format_exc())
|
|
991
|
-
_emit("", "error", message=
|
|
1034
|
+
_emit("", "error", message="The assistant could not start on this runner. " + _turn_error_message(exc))
|
|
992
1035
|
return 1
|
|
993
1036
|
# Capture the freshly-built BASE system prompt (before any static context is
|
|
994
1037
|
# folded in) so each turn can deterministically rebuild base + persona.
|
package/dist/cli.js
CHANGED
|
@@ -17,14 +17,6 @@ import { Command } from "commander";
|
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import { connect } from "./connection.js";
|
|
19
19
|
import { detectModels } from "./detect.js";
|
|
20
|
-
const program = new Command()
|
|
21
|
-
.name("melaya-runner")
|
|
22
|
-
.description("Run Melaya AI pipelines locally with your own models")
|
|
23
|
-
.requiredOption("--token <token>", "Runner token from the Melaya platform")
|
|
24
|
-
.option("--server <url>", "Server URL", "wss://api.melaya.org")
|
|
25
|
-
.option("--verbose", "Show detailed logs", false)
|
|
26
|
-
.parse(process.argv);
|
|
27
|
-
const opts = program.opts();
|
|
28
20
|
// Logo colors: cyan → blue → indigo gradient (matching melaya.org)
|
|
29
21
|
const c1 = chalk.hex("#22D3EE").bold;
|
|
30
22
|
const c2 = chalk.hex("#10B0F0").bold;
|
|
@@ -40,7 +32,7 @@ import { dirname, join } from "path";
|
|
|
40
32
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
41
33
|
const { version } = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
42
34
|
const BANNER = `
|
|
43
|
-
${c1("███╗ ███╗")} ${c2("███████╗")} ${c3("██╗")}
|
|
35
|
+
${c1("███╗ ███╗")} ${c2("███████╗")} ${c3("██╗")} ${c4("█████╗")} ${c5("██╗ ██╗")} ${c6("█████╗")}
|
|
44
36
|
${c1("████╗ ████║")} ${c2("██╔════╝")} ${c3("██║")} ${c4("██╔══██╗")} ${c5("╚██╗ ██╔╝")} ${c6("██╔══██╗")}
|
|
45
37
|
${c1("██╔████╔██║")} ${c2("█████╗")} ${c3("██║")} ${c4("███████║")} ${c5("╚████╔╝")} ${c6("███████║")}
|
|
46
38
|
${c1("██║╚██╔╝██║")} ${c2("██╔══╝")} ${c3("██║")} ${c4("██╔══██║")} ${c5("╚██╔╝")} ${c6("██╔══██║")}
|
|
@@ -51,6 +43,14 @@ const BANNER = `
|
|
|
51
43
|
${br("─────────────────────────────────────────────────────")}
|
|
52
44
|
`;
|
|
53
45
|
async function main() {
|
|
46
|
+
const program = new Command()
|
|
47
|
+
.name("melaya-runner")
|
|
48
|
+
.description("Run Melaya AI pipelines locally with your own models")
|
|
49
|
+
.requiredOption("--token <token>", "Runner token from the Melaya platform")
|
|
50
|
+
.option("--server <url>", "Server URL", "wss://api.melaya.org")
|
|
51
|
+
.option("--verbose", "Show detailed logs", false)
|
|
52
|
+
.parse(process.argv);
|
|
53
|
+
const opts = program.opts();
|
|
54
54
|
console.log(BANNER);
|
|
55
55
|
// Detect Python
|
|
56
56
|
const python = await findPython();
|
|
@@ -63,8 +63,8 @@ async function main() {
|
|
|
63
63
|
// Detect local models
|
|
64
64
|
const models = await detectModels();
|
|
65
65
|
if (models.length === 0) {
|
|
66
|
-
console.log(chalk.yellow(" ⚠ No local models detected (LM Studio / Ollama not running
|
|
67
|
-
console.log(chalk.gray(" Start LM Studio or Ollama
|
|
66
|
+
console.log(chalk.yellow(" ⚠ No local models detected (LM Studio / Ollama not running; Claude Code / Codex / GitHub Copilot not logged in)"));
|
|
67
|
+
console.log(chalk.gray(" Start LM Studio or Ollama, log in to Claude Code / Codex, or run `melaya-runner copilot login`, then restart the runner"));
|
|
68
68
|
}
|
|
69
69
|
else {
|
|
70
70
|
for (const m of models) {
|
|
@@ -93,7 +93,17 @@ async function findPython() {
|
|
|
93
93
|
}
|
|
94
94
|
return null;
|
|
95
95
|
}
|
|
96
|
-
|
|
96
|
+
// `melaya-runner copilot login` — GitHub Copilot device-flow sign-in, cached
|
|
97
|
+
// locally. Handled before the runner arg-parse so it needs no --token.
|
|
98
|
+
async function bootstrap() {
|
|
99
|
+
if (process.argv[2] === "copilot") {
|
|
100
|
+
const { copilotLogin } = await import("./copilotLogin.js");
|
|
101
|
+
await copilotLogin();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
await main();
|
|
105
|
+
}
|
|
106
|
+
bootstrap().catch((e) => {
|
|
97
107
|
console.error(chalk.red(`\n Fatal: ${e.message}\n`));
|
|
98
108
|
process.exit(1);
|
|
99
109
|
});
|
package/dist/connection.js
CHANGED
|
@@ -1478,7 +1478,7 @@ export async function connect(opts) {
|
|
|
1478
1478
|
socket.on("runner:assistant_turn", async (payload) => {
|
|
1479
1479
|
const s = activeAssistants.get(String(payload.sessionId || ""));
|
|
1480
1480
|
if (!s) {
|
|
1481
|
-
socket.emit("runner:assistant_event", { sessionId: payload.sessionId, turnId: payload.turnId, kind: "error", message: "
|
|
1481
|
+
socket.emit("runner:assistant_event", { sessionId: payload.sessionId, turnId: payload.turnId, kind: "error", message: "The assistant session on your runner is gone (it most likely restarted). Send the message again to start a fresh one." });
|
|
1482
1482
|
return;
|
|
1483
1483
|
}
|
|
1484
1484
|
// Generation gate (defense-in-depth): refuse to serve a turn stamped with a
|
|
@@ -1487,7 +1487,7 @@ export async function connect(opts) {
|
|
|
1487
1487
|
// triggers the server's clean re-boot path. Backward-compatible: a turn with
|
|
1488
1488
|
// no generation (older server) is served as before.
|
|
1489
1489
|
if (payload.generation != null && Number(payload.generation) !== s.generation) {
|
|
1490
|
-
socket.emit("runner:assistant_event", { sessionId: payload.sessionId, turnId: payload.turnId, kind: "error", message: "
|
|
1490
|
+
socket.emit("runner:assistant_event", { sessionId: payload.sessionId, turnId: payload.turnId, kind: "error", message: "The assistant session on your runner is gone (it most likely restarted). Send the message again to start a fresh one." });
|
|
1491
1491
|
return;
|
|
1492
1492
|
}
|
|
1493
1493
|
s.lastActivity = Date.now();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function copilotLogin(): Promise<void>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Copilot device-flow login for the runner.
|
|
3
|
+
*
|
|
4
|
+
* github_copilot is a runner CLI provider (like claude_code/codex): the runtime
|
|
5
|
+
* reads a GitHub OAuth token from disk on THIS machine. The standalone
|
|
6
|
+
* `@github/copilot` CLI stores its token via MSAL (encrypted, unreadable), so
|
|
7
|
+
* for users who don't already have a readable token (editor plugin apps.json /
|
|
8
|
+
* gh), this command runs GitHub's device flow ONCE and caches the durable
|
|
9
|
+
* `gho_` token at ~/.melaya-runner/github_copilot.json — the same file the
|
|
10
|
+
* runtime (model.py::_read_github_copilot_token) and detect.ts read.
|
|
11
|
+
*
|
|
12
|
+
* Nothing here touches the Melaya cloud: the token is minted against GitHub and
|
|
13
|
+
* stored locally. Run: `npx @melaya/runner copilot login`.
|
|
14
|
+
*/
|
|
15
|
+
import { writeFile, mkdir } from "fs/promises";
|
|
16
|
+
import { homedir } from "os";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
const CLIENT_ID = "Iv1.b507a08c87ecfe98"; // the well-known Copilot (copilot.vim) device-flow client id
|
|
20
|
+
const UA = "GithubCopilot/1.155.0";
|
|
21
|
+
export async function copilotLogin() {
|
|
22
|
+
console.log(chalk.cyan("\n GitHub Copilot — device sign-in\n"));
|
|
23
|
+
// 1) Request a device code.
|
|
24
|
+
const startRes = await fetch("https://github.com/login/device/code", {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", "User-Agent": UA },
|
|
27
|
+
body: new URLSearchParams({ client_id: CLIENT_ID, scope: "read:user" }),
|
|
28
|
+
});
|
|
29
|
+
if (!startRes.ok) {
|
|
30
|
+
console.log(chalk.red(` ✗ Could not start GitHub device flow (HTTP ${startRes.status}).`));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const dev = (await startRes.json());
|
|
34
|
+
const interval = Math.max(1, Number(dev.interval) || 5);
|
|
35
|
+
const deadline = Date.now() + Math.max(60, Number(dev.expires_in) || 900) * 1000;
|
|
36
|
+
console.log(` 1. Open: ${chalk.underline(dev.verification_uri || "https://github.com/login/device")}`);
|
|
37
|
+
console.log(` 2. Enter: ${chalk.bold.yellow(dev.user_code)}`);
|
|
38
|
+
console.log(chalk.gray("\n Waiting for you to authorize…\n"));
|
|
39
|
+
// 2) Poll for the token.
|
|
40
|
+
let token = "";
|
|
41
|
+
while (Date.now() < deadline) {
|
|
42
|
+
await new Promise((r) => setTimeout(r, interval * 1000));
|
|
43
|
+
const pollRes = await fetch("https://github.com/login/oauth/access_token", {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", "User-Agent": UA },
|
|
46
|
+
body: new URLSearchParams({
|
|
47
|
+
client_id: CLIENT_ID,
|
|
48
|
+
device_code: dev.device_code,
|
|
49
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
const data = (await pollRes.json().catch(() => ({})));
|
|
53
|
+
if (data.access_token) {
|
|
54
|
+
token = data.access_token;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
if (data.error && data.error !== "authorization_pending" && data.error !== "slow_down") {
|
|
58
|
+
console.log(chalk.red(` ✗ ${data.error === "expired_token" ? "The code expired — run the command again." : data.error === "access_denied" ? "Sign-in was denied." : `Sign-in failed (${data.error}).`}`));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!token) {
|
|
63
|
+
console.log(chalk.red(" ✗ Timed out waiting for authorization. Run the command again."));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
// 3) Confirm the token actually has a Copilot seat (exchange), then cache it.
|
|
67
|
+
let login = "";
|
|
68
|
+
try {
|
|
69
|
+
const ex = await fetch("https://api.github.com/copilot_internal/v2/token", {
|
|
70
|
+
headers: { Authorization: `token ${token}`, "User-Agent": UA, "Editor-Version": "vscode/1.95.0", "Editor-Plugin-Version": "copilot-chat/0.22.0" },
|
|
71
|
+
});
|
|
72
|
+
if (!ex.ok) {
|
|
73
|
+
console.log(chalk.red(` ✗ Signed in, but this GitHub account has no active Copilot subscription (HTTP ${ex.status}).`));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch { /* transient — still cache; the runtime re-exchanges */ }
|
|
78
|
+
try {
|
|
79
|
+
const who = await fetch("https://api.github.com/user", { headers: { Authorization: `token ${token}`, "User-Agent": UA, Accept: "application/vnd.github+json" } });
|
|
80
|
+
if (who.ok)
|
|
81
|
+
login = String((await who.json()).login || "");
|
|
82
|
+
}
|
|
83
|
+
catch { /* optional */ }
|
|
84
|
+
const dir = join(homedir(), ".melaya-runner");
|
|
85
|
+
await mkdir(dir, { recursive: true });
|
|
86
|
+
const file = join(dir, "github_copilot.json");
|
|
87
|
+
await writeFile(file, JSON.stringify({ oauth_token: token, user: login, host: "github.com" }, null, 2), { mode: 0o600 });
|
|
88
|
+
console.log(chalk.green(`\n ✓ GitHub Copilot connected${login ? ` as ${login}` : ""}.`));
|
|
89
|
+
console.log(chalk.gray(` Token cached at ${file} (this machine only). Restart the runner to pick it up.\n`));
|
|
90
|
+
}
|
package/dist/detect.d.ts
CHANGED
package/dist/detect.js
CHANGED
|
@@ -8,6 +8,7 @@ import { readFile } from "fs/promises";
|
|
|
8
8
|
import { homedir } from "os";
|
|
9
9
|
import { join } from "path";
|
|
10
10
|
const execFileAsync = promisify(execFile);
|
|
11
|
+
const COPILOT_MODELS_FALLBACK = ["gpt-4o", "gpt-4.1", "o3-mini", "claude-3.5-sonnet", "gemini-2.0-flash"];
|
|
11
12
|
/** Fallbacks used ONLY when live discovery fails (offline runner, transient
|
|
12
13
|
* error, missing cache). The REAL lists are fetched dynamically below so a new
|
|
13
14
|
* claude/codex release is available on Melaya automatically — no code change,
|
|
@@ -136,6 +137,94 @@ async function detectCodex() {
|
|
|
136
137
|
catch { /* malformed creds → treat as logged out */ }
|
|
137
138
|
return []; // not logged in → don't advertise models
|
|
138
139
|
}
|
|
140
|
+
/** Read the user's GitHub OAuth token for Copilot from a readable source on
|
|
141
|
+
* THIS machine, in priority order. The standalone `@github/copilot` CLI stores
|
|
142
|
+
* its token via MSAL (encrypted, unreadable), so we read the editor-plugin
|
|
143
|
+
* token, the gh CLI token, or a token cached by the runner's own device-flow
|
|
144
|
+
* login (`melaya-runner copilot login` → ~/.melaya-runner/github_copilot.json).
|
|
145
|
+
* Mirrors model.py::_read_github_copilot_token — keep the source list in sync. */
|
|
146
|
+
async function readGithubCopilotToken() {
|
|
147
|
+
// 1) Editor plugins (copilot.vim / JetBrains / Neovim): plaintext oauth_token.
|
|
148
|
+
for (const rel of [
|
|
149
|
+
join(homedir(), ".config", "github-copilot", "apps.json"),
|
|
150
|
+
join(homedir(), ".config", "github-copilot", "hosts.json"),
|
|
151
|
+
]) {
|
|
152
|
+
try {
|
|
153
|
+
const j = JSON.parse(await readFile(rel, "utf8"));
|
|
154
|
+
for (const v of Object.values(j)) {
|
|
155
|
+
if (v?.oauth_token)
|
|
156
|
+
return v.oauth_token;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch { /* absent → next source */ }
|
|
160
|
+
}
|
|
161
|
+
// 2) Runner-cached token from our own device-flow login.
|
|
162
|
+
try {
|
|
163
|
+
const j = JSON.parse(await readFile(join(homedir(), ".melaya-runner", "github_copilot.json"), "utf8"));
|
|
164
|
+
if (j?.oauth_token)
|
|
165
|
+
return j.oauth_token;
|
|
166
|
+
}
|
|
167
|
+
catch { /* not logged in via runner */ }
|
|
168
|
+
// 3) gh CLI env token (broad-scope PAT/OAuth; works for the exchange).
|
|
169
|
+
if (process.env.GH_TOKEN)
|
|
170
|
+
return process.env.GH_TOKEN;
|
|
171
|
+
if (process.env.GITHUB_TOKEN)
|
|
172
|
+
return process.env.GITHUB_TOKEN;
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
/** GitHub Copilot as a runner CLI provider: when a readable GitHub OAuth token
|
|
176
|
+
* is present on THIS machine, exchange it for a short-lived Copilot token and
|
|
177
|
+
* list the models the subscription serves, so the picker shows TRUE local
|
|
178
|
+
* availability (copilot pipelines execute HERE, same as claude_code/codex). */
|
|
179
|
+
async function detectGithubCopilot() {
|
|
180
|
+
const gh = await readGithubCopilotToken();
|
|
181
|
+
if (!gh)
|
|
182
|
+
return [];
|
|
183
|
+
try {
|
|
184
|
+
const ex = await fetch("https://api.github.com/copilot_internal/v2/token", {
|
|
185
|
+
headers: {
|
|
186
|
+
Authorization: `token ${gh}`,
|
|
187
|
+
"User-Agent": "GithubCopilot/1.155.0",
|
|
188
|
+
"Editor-Version": "vscode/1.95.0",
|
|
189
|
+
"Editor-Plugin-Version": "copilot-chat/0.22.0",
|
|
190
|
+
},
|
|
191
|
+
signal: AbortSignal.timeout(6000),
|
|
192
|
+
});
|
|
193
|
+
if (!ex.ok)
|
|
194
|
+
return []; // no Copilot seat / revoked → don't advertise
|
|
195
|
+
const exd = (await ex.json());
|
|
196
|
+
if (!exd.token)
|
|
197
|
+
return [];
|
|
198
|
+
const base = (exd.endpoints?.api || "https://api.githubcopilot.com").replace(/\/+$/, "");
|
|
199
|
+
try {
|
|
200
|
+
const r = await fetch(`${base}/models`, {
|
|
201
|
+
headers: {
|
|
202
|
+
Authorization: `Bearer ${exd.token}`,
|
|
203
|
+
"Copilot-Integration-Id": "vscode-chat",
|
|
204
|
+
"Editor-Version": "vscode/1.95.0",
|
|
205
|
+
"Editor-Plugin-Version": "copilot-chat/0.22.0",
|
|
206
|
+
},
|
|
207
|
+
signal: AbortSignal.timeout(6000),
|
|
208
|
+
});
|
|
209
|
+
if (r.ok) {
|
|
210
|
+
const d = (await r.json());
|
|
211
|
+
// Keep ONLY real chat models the agent runtime can drive (tool calling
|
|
212
|
+
// is required), and drop Copilot-internal routing/search/compaction ids
|
|
213
|
+
// and embedding models that the /models list also advertises.
|
|
214
|
+
const ids = (d.data ?? [])
|
|
215
|
+
.filter((m) => m?.capabilities?.type === "chat" && m?.capabilities?.supports?.tool_calls === true)
|
|
216
|
+
.map((m) => m?.id ?? "")
|
|
217
|
+
.filter((s) => s && !/^copilot-search|^exec-agent|^trajectory-compaction|-free-auto$|-flash-(picker|secondary|tertiary)$|embedding/i.test(s));
|
|
218
|
+
if (ids.length)
|
|
219
|
+
return ids.map((name) => ({ provider: "github_copilot", name }));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch { /* models list failed → fallback below */ }
|
|
223
|
+
return COPILOT_MODELS_FALLBACK.map((name) => ({ provider: "github_copilot", name }));
|
|
224
|
+
}
|
|
225
|
+
catch { /* offline / transient → not advertised */ }
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
139
228
|
export async function detectModels() {
|
|
140
229
|
const models = [];
|
|
141
230
|
// LM Studio — OpenAI-compatible at :1234
|
|
@@ -164,5 +253,7 @@ export async function detectModels() {
|
|
|
164
253
|
models.push(...await detectClaudeCode());
|
|
165
254
|
// OpenAI Codex CLI — subscription auth on THIS machine
|
|
166
255
|
models.push(...await detectCodex());
|
|
256
|
+
// GitHub Copilot — GitHub OAuth token readable on THIS machine
|
|
257
|
+
models.push(...await detectGithubCopilot());
|
|
167
258
|
return models;
|
|
168
259
|
}
|