@melaya/runner 1.0.73 → 1.0.76
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/detect.d.ts +1 -1
- package/dist/detect.js +128 -21
- package/package.json +1 -1
package/dist/detect.d.ts
CHANGED
package/dist/detect.js
CHANGED
|
@@ -4,32 +4,137 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { execFile } from "child_process";
|
|
6
6
|
import { promisify } from "util";
|
|
7
|
+
import { readFile } from "fs/promises";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { join } from "path";
|
|
7
10
|
const execFileAsync = promisify(execFile);
|
|
8
|
-
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
/** Fallbacks used ONLY when live discovery fails (offline runner, transient
|
|
12
|
+
* error, missing cache). The REAL lists are fetched dynamically below so a new
|
|
13
|
+
* claude/codex release is available on Melaya automatically — no code change,
|
|
14
|
+
* no redeploy. Kept minimal + in step with the registry `staticModels`. */
|
|
15
|
+
const CODEX_MODELS_FALLBACK = ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"];
|
|
16
|
+
const CLAUDE_ALIASES_FALLBACK = ["sonnet", "opus", "haiku"];
|
|
17
|
+
/** Discover the claude model FAMILIES this subscription serves, so a new
|
|
18
|
+
* family (a future tier) appears on Melaya automatically. Hits the same
|
|
19
|
+
* GET /v1/models the runtime resolver uses (model.py::_resolve_claude_model_id)
|
|
20
|
+
* with the OAuth token, then reduces concrete ids (claude-<family>-<ver>-<date>)
|
|
21
|
+
* to their family aliases (opus/sonnet/haiku/…). Each alias resolves to the
|
|
22
|
+
* NEWEST model of its family at call time, so within-family releases are picked
|
|
23
|
+
* up for free too. Best-effort: any failure falls back to the known aliases. */
|
|
24
|
+
async function fetchClaudeFamilies(token) {
|
|
25
|
+
try {
|
|
26
|
+
const r = await fetch("https://api.anthropic.com/v1/models?limit=100", {
|
|
27
|
+
headers: {
|
|
28
|
+
Authorization: `Bearer ${token}`,
|
|
29
|
+
"anthropic-version": "2023-06-01",
|
|
30
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
31
|
+
"x-app": "cli",
|
|
32
|
+
"User-Agent": "claude-code/2.1.81",
|
|
33
|
+
},
|
|
34
|
+
signal: AbortSignal.timeout(6000),
|
|
35
|
+
});
|
|
36
|
+
if (r.ok) {
|
|
37
|
+
const data = (await r.json());
|
|
38
|
+
const fams = new Set();
|
|
39
|
+
for (const m of data.data ?? []) {
|
|
40
|
+
const mm = /^claude-([a-z]+)-/.exec(m?.id ?? "");
|
|
41
|
+
if (mm)
|
|
42
|
+
fams.add(mm[1]);
|
|
43
|
+
}
|
|
44
|
+
if (fams.size)
|
|
45
|
+
return [...fams];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch { /* offline / transient → fallback */ }
|
|
49
|
+
return CLAUDE_ALIASES_FALLBACK;
|
|
50
|
+
}
|
|
51
|
+
/** Read the codex model catalog from the CLI's OWN auto-refreshed cache
|
|
52
|
+
* (~/.codex/models_cache.json, written by codex when it lists models), so a
|
|
53
|
+
* new codex release appears on Melaya automatically. File-based like auth.json
|
|
54
|
+
* — no API call, no subprocess/terminal. Filters non-chat helper slugs
|
|
55
|
+
* (e.g. codex-auto-review). Falls back to a known set. */
|
|
56
|
+
async function readCodexModels() {
|
|
57
|
+
try {
|
|
58
|
+
const raw = await readFile(join(homedir(), ".codex", "models_cache.json"), "utf8");
|
|
59
|
+
const j = JSON.parse(raw);
|
|
60
|
+
const slugs = (j.models ?? [])
|
|
61
|
+
.map((m) => m?.slug ?? "")
|
|
62
|
+
.filter((s) => s && !/(^|-)(auto|review|embed|moderation)(-|$)/i.test(s));
|
|
63
|
+
if (slugs.length)
|
|
64
|
+
return slugs;
|
|
65
|
+
}
|
|
66
|
+
catch { /* no cache / malformed → fallback */ }
|
|
67
|
+
return CODEX_MODELS_FALLBACK;
|
|
68
|
+
}
|
|
69
|
+
/** Claude Code is a subscription CLI — when it's logged in on this machine,
|
|
70
|
+
* report its model aliases so the platform's model picker shows TRUE
|
|
71
|
+
* availability for local runs (claude_code pipelines execute HERE).
|
|
72
|
+
*
|
|
73
|
+
* We read the CLI's OAuth credentials FILE directly rather than spawning
|
|
74
|
+
* `claude auth status`. That subprocess flashed a console window on Windows
|
|
75
|
+
* every time the runner started (`windowsHide` is unreliable through the
|
|
76
|
+
* `.cmd` shell wrapper, which spawns its own conhost). The file is the source
|
|
77
|
+
* of truth on Windows/Linux; macOS keeps the grant in the Keychain, where
|
|
78
|
+
* `security` is a subprocess but pops NO window. Zero windows, faster, and
|
|
79
|
+
* it mirrors exactly what the runtime (model.py `_read_claude_oauth`) reads. */
|
|
12
80
|
async function detectClaudeCode() {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
81
|
+
let raw = null;
|
|
82
|
+
try {
|
|
83
|
+
raw = await readFile(join(homedir(), ".claude", ".credentials.json"), "utf8");
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
raw = null;
|
|
87
|
+
}
|
|
88
|
+
if (!raw && process.platform === "darwin") {
|
|
17
89
|
try {
|
|
18
|
-
const { stdout } = await execFileAsync(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
90
|
+
const { stdout } = await execFileAsync("security", ["find-generic-password", "-s", "Claude Code-credentials", "-w"], { timeout: 8000 });
|
|
91
|
+
if (stdout && stdout.trim())
|
|
92
|
+
raw = stdout.trim();
|
|
93
|
+
}
|
|
94
|
+
catch { /* not in keychain */ }
|
|
95
|
+
}
|
|
96
|
+
if (!raw)
|
|
97
|
+
return [];
|
|
98
|
+
try {
|
|
99
|
+
const oauth = (JSON.parse(raw)?.claudeAiOauth ?? {});
|
|
100
|
+
const expiresAt = Number(oauth.expiresAt ?? 0);
|
|
101
|
+
if (oauth.accessToken && (!expiresAt || Date.now() < expiresAt)) {
|
|
102
|
+
const names = await fetchClaudeFamilies(oauth.accessToken);
|
|
103
|
+
return names.map((name) => ({ provider: "claude_code", name }));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch { /* malformed creds → treat as logged out */ }
|
|
107
|
+
return []; // not logged in / expired → don't advertise models
|
|
108
|
+
}
|
|
109
|
+
/** OpenAI Codex CLI is a subscription CLI (ChatGPT Plus/Pro) — when it's
|
|
110
|
+
* logged in on this machine, advertise its models so the platform's picker
|
|
111
|
+
* shows TRUE availability for local runs (codex pipelines execute HERE).
|
|
112
|
+
*
|
|
113
|
+
* Same file-based approach as `detectClaudeCode`: read `~/.codex/auth.json`
|
|
114
|
+
* directly rather than spawning `codex login status`. The subprocess flashed
|
|
115
|
+
* a console window on Windows on every runner start (`windowsHide` is
|
|
116
|
+
* unreliable through the `.cmd` shell wrapper). The file is the source of
|
|
117
|
+
* truth cross-platform and mirrors what the runtime (model.py) authenticates
|
|
118
|
+
* against. A ChatGPT login stores `tokens.access_token`; an API-key login
|
|
119
|
+
* stores `OPENAI_API_KEY`. Either means "logged in". */
|
|
120
|
+
async function detectCodex() {
|
|
121
|
+
let raw = null;
|
|
122
|
+
try {
|
|
123
|
+
raw = await readFile(join(homedir(), ".codex", "auth.json"), "utf8");
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const auth = JSON.parse(raw);
|
|
130
|
+
const loggedIn = !!(auth?.tokens?.access_token || auth?.OPENAI_API_KEY);
|
|
131
|
+
if (loggedIn) {
|
|
132
|
+
const names = await readCodexModels();
|
|
133
|
+
return names.map((name) => ({ provider: "codex", name }));
|
|
29
134
|
}
|
|
30
|
-
catch { /* not installed / not on PATH / non-JSON — try next candidate */ }
|
|
31
135
|
}
|
|
32
|
-
|
|
136
|
+
catch { /* malformed creds → treat as logged out */ }
|
|
137
|
+
return []; // not logged in → don't advertise models
|
|
33
138
|
}
|
|
34
139
|
export async function detectModels() {
|
|
35
140
|
const models = [];
|
|
@@ -57,5 +162,7 @@ export async function detectModels() {
|
|
|
57
162
|
catch { /* not running */ }
|
|
58
163
|
// Claude Code CLI — subscription auth on THIS machine
|
|
59
164
|
models.push(...await detectClaudeCode());
|
|
165
|
+
// OpenAI Codex CLI — subscription auth on THIS machine
|
|
166
|
+
models.push(...await detectCodex());
|
|
60
167
|
return models;
|
|
61
168
|
}
|