@kenkaiiii/gg-core 5.1.2 → 5.3.0
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/chunk-IFKGCWX2.js +1795 -0
- package/dist/chunk-IFKGCWX2.js.map +1 -0
- package/dist/index.cjs +706 -650
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.js +42 -1336
- package/dist/index.js.map +1 -1
- package/dist/model-registry.cjs +104 -7
- package/dist/model-registry.cjs.map +1 -1
- package/dist/model-registry.d.cts +30 -3
- package/dist/model-registry.d.ts +30 -3
- package/dist/model-registry.js +6 -1
- package/package.json +2 -2
- package/dist/chunk-7GQQLIPG.js +0 -390
- package/dist/chunk-7GQQLIPG.js.map +0 -1
|
@@ -0,0 +1,1795 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getAppPaths
|
|
3
|
+
} from "./chunk-KQJNZC6A.js";
|
|
4
|
+
|
|
5
|
+
// src/auth-storage.ts
|
|
6
|
+
import fs4 from "fs/promises";
|
|
7
|
+
import crypto5 from "crypto";
|
|
8
|
+
|
|
9
|
+
// src/oauth/anthropic.ts
|
|
10
|
+
import crypto2 from "crypto";
|
|
11
|
+
|
|
12
|
+
// src/oauth/pkce.ts
|
|
13
|
+
function base64urlEncode(bytes) {
|
|
14
|
+
let binary = "";
|
|
15
|
+
for (const byte of bytes) {
|
|
16
|
+
binary += String.fromCharCode(byte);
|
|
17
|
+
}
|
|
18
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
19
|
+
}
|
|
20
|
+
async function generatePKCE() {
|
|
21
|
+
const verifierBytes = new Uint8Array(32);
|
|
22
|
+
crypto.getRandomValues(verifierBytes);
|
|
23
|
+
const verifier = base64urlEncode(verifierBytes);
|
|
24
|
+
const data = new TextEncoder().encode(verifier);
|
|
25
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
26
|
+
const challenge = base64urlEncode(new Uint8Array(hashBuffer));
|
|
27
|
+
return { verifier, challenge };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/claude-code-version.ts
|
|
31
|
+
import fs2 from "fs/promises";
|
|
32
|
+
import path2 from "path";
|
|
33
|
+
|
|
34
|
+
// src/logger.ts
|
|
35
|
+
import fs from "fs";
|
|
36
|
+
import path from "path";
|
|
37
|
+
import { randomBytes } from "crypto";
|
|
38
|
+
var MAX_BYTES = 10 * 1024 * 1024;
|
|
39
|
+
var fd = null;
|
|
40
|
+
var sessionId = "";
|
|
41
|
+
var appName = "app";
|
|
42
|
+
var cleanups = [];
|
|
43
|
+
function rotateIfNeeded(filePath) {
|
|
44
|
+
try {
|
|
45
|
+
const st = fs.statSync(filePath);
|
|
46
|
+
if (st.size < MAX_BYTES) return;
|
|
47
|
+
const rotated = `${filePath}.1`;
|
|
48
|
+
try {
|
|
49
|
+
fs.unlinkSync(rotated);
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
fs.renameSync(filePath, rotated);
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function openLog(filePath, name) {
|
|
57
|
+
if (fd !== null) return false;
|
|
58
|
+
appName = name;
|
|
59
|
+
try {
|
|
60
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 448 });
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
rotateIfNeeded(filePath);
|
|
64
|
+
try {
|
|
65
|
+
fd = fs.openSync(filePath, "a");
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
sessionId = randomBytes(4).toString("hex");
|
|
70
|
+
try {
|
|
71
|
+
fs.writeSync(fd, "\n");
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
function getSessionId() {
|
|
77
|
+
return sessionId;
|
|
78
|
+
}
|
|
79
|
+
function isLoggerOpen() {
|
|
80
|
+
return fd !== null;
|
|
81
|
+
}
|
|
82
|
+
function log(level, category, message, data) {
|
|
83
|
+
if (fd === null) return;
|
|
84
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
85
|
+
let line = `[${ts}] [sid=${sessionId}] [${level}] [${category}] ${message}`;
|
|
86
|
+
if (data) {
|
|
87
|
+
const pairs = Object.entries(data).map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(" ");
|
|
88
|
+
if (pairs) line += ` ${pairs}`;
|
|
89
|
+
}
|
|
90
|
+
line += "\n";
|
|
91
|
+
try {
|
|
92
|
+
fs.writeSync(fd, line);
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function registerLogCleanup(fn) {
|
|
97
|
+
cleanups.push(fn);
|
|
98
|
+
}
|
|
99
|
+
function closeLogger(opts) {
|
|
100
|
+
if (fd === null) return;
|
|
101
|
+
if (opts?.shutdownLine !== false) log("INFO", "shutdown", `${appName} shutting down`);
|
|
102
|
+
try {
|
|
103
|
+
fs.closeSync(fd);
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
fd = null;
|
|
107
|
+
for (const unsub of cleanups) unsub();
|
|
108
|
+
cleanups = [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/claude-code-version.ts
|
|
112
|
+
var NPM_LATEST_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
|
|
113
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
114
|
+
var FETCH_TIMEOUT_MS = 3e3;
|
|
115
|
+
var FALLBACK_VERSION = "2.1.88";
|
|
116
|
+
var memoryCache = null;
|
|
117
|
+
var inflight = null;
|
|
118
|
+
function cachePath() {
|
|
119
|
+
return path2.join(getAppPaths().agentDir, "claude-code-version.json");
|
|
120
|
+
}
|
|
121
|
+
async function readDiskCache() {
|
|
122
|
+
try {
|
|
123
|
+
const raw = await fs2.readFile(cachePath(), "utf-8");
|
|
124
|
+
const parsed = JSON.parse(raw);
|
|
125
|
+
if (typeof parsed.version === "string" && typeof parsed.fetchedAt === "number") {
|
|
126
|
+
return parsed;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function writeDiskCache(data) {
|
|
134
|
+
try {
|
|
135
|
+
await fs2.mkdir(getAppPaths().agentDir, { recursive: true, mode: 448 });
|
|
136
|
+
await fs2.writeFile(cachePath(), JSON.stringify(data), { mode: 384 });
|
|
137
|
+
} catch (err) {
|
|
138
|
+
log(
|
|
139
|
+
"WARN",
|
|
140
|
+
"claude-code-version",
|
|
141
|
+
`Failed to write cache: ${err instanceof Error ? err.message : String(err)}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function fetchLatest() {
|
|
146
|
+
const controller = new AbortController();
|
|
147
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
148
|
+
try {
|
|
149
|
+
const response = await fetch(NPM_LATEST_URL, { signal: controller.signal });
|
|
150
|
+
if (!response.ok) return null;
|
|
151
|
+
const data = await response.json();
|
|
152
|
+
if (typeof data.version === "string" && /^\d/.test(data.version)) {
|
|
153
|
+
return data.version;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
} finally {
|
|
159
|
+
clearTimeout(timer);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function getClaudeCodeVersion() {
|
|
163
|
+
if (memoryCache && Date.now() < memoryCache.expiresAt) {
|
|
164
|
+
return memoryCache.version;
|
|
165
|
+
}
|
|
166
|
+
if (inflight) return inflight;
|
|
167
|
+
inflight = (async () => {
|
|
168
|
+
const disk = await readDiskCache();
|
|
169
|
+
const diskFresh = disk && Date.now() - disk.fetchedAt < CACHE_TTL_MS;
|
|
170
|
+
if (disk && diskFresh) {
|
|
171
|
+
memoryCache = { version: disk.version, expiresAt: Date.now() + CACHE_TTL_MS };
|
|
172
|
+
return disk.version;
|
|
173
|
+
}
|
|
174
|
+
const fetched = await fetchLatest();
|
|
175
|
+
if (fetched) {
|
|
176
|
+
await writeDiskCache({ version: fetched, fetchedAt: Date.now() });
|
|
177
|
+
memoryCache = { version: fetched, expiresAt: Date.now() + CACHE_TTL_MS };
|
|
178
|
+
return fetched;
|
|
179
|
+
}
|
|
180
|
+
const resolved = disk?.version ?? FALLBACK_VERSION;
|
|
181
|
+
memoryCache = { version: resolved, expiresAt: Date.now() + 5 * 60 * 1e3 };
|
|
182
|
+
log(
|
|
183
|
+
"WARN",
|
|
184
|
+
"claude-code-version",
|
|
185
|
+
`Failed to fetch latest Claude Code version; using ${resolved}`
|
|
186
|
+
);
|
|
187
|
+
return resolved;
|
|
188
|
+
})();
|
|
189
|
+
try {
|
|
190
|
+
return await inflight;
|
|
191
|
+
} finally {
|
|
192
|
+
inflight = null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function getClaudeCliUserAgent() {
|
|
196
|
+
const version = await getClaudeCodeVersion();
|
|
197
|
+
return `claude-cli/${version} (external, cli)`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/oauth/anthropic.ts
|
|
201
|
+
var CLIENT_ID = atob("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl");
|
|
202
|
+
var AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
203
|
+
var TOKEN_URLS = [
|
|
204
|
+
"https://platform.claude.com/v1/oauth/token",
|
|
205
|
+
"https://console.anthropic.com/v1/oauth/token"
|
|
206
|
+
];
|
|
207
|
+
var REDIRECT_URI = "https://platform.claude.com/oauth/code/callback";
|
|
208
|
+
var SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
|
|
209
|
+
async function postTokenRequest(body, label) {
|
|
210
|
+
const encoded = JSON.stringify(body);
|
|
211
|
+
const headers = {
|
|
212
|
+
"Content-Type": "application/json",
|
|
213
|
+
"User-Agent": await getClaudeCliUserAgent(),
|
|
214
|
+
"anthropic-beta": "oauth-2025-04-20"
|
|
215
|
+
};
|
|
216
|
+
let lastError = null;
|
|
217
|
+
for (const url of TOKEN_URLS) {
|
|
218
|
+
let response;
|
|
219
|
+
try {
|
|
220
|
+
response = await fetch(url, { method: "POST", headers, body: encoded });
|
|
221
|
+
} catch (err) {
|
|
222
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (response.ok) {
|
|
226
|
+
return await response.json();
|
|
227
|
+
}
|
|
228
|
+
const text = await response.text();
|
|
229
|
+
if (response.status >= 400 && response.status < 500) {
|
|
230
|
+
throw new Error(`Anthropic ${label} failed (${response.status}): ${text}`);
|
|
231
|
+
}
|
|
232
|
+
lastError = new Error(`Anthropic ${label} failed (${response.status}): ${text}`);
|
|
233
|
+
}
|
|
234
|
+
throw lastError ?? new Error(`Anthropic ${label} failed: all endpoints unreachable`);
|
|
235
|
+
}
|
|
236
|
+
function toCredentials(data) {
|
|
237
|
+
return {
|
|
238
|
+
accessToken: data.access_token,
|
|
239
|
+
refreshToken: data.refresh_token,
|
|
240
|
+
expiresAt: Date.now() + data.expires_in * 1e3 - 5 * 60 * 1e3
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
async function loginAnthropic(callbacks) {
|
|
244
|
+
const { verifier, challenge } = await generatePKCE();
|
|
245
|
+
const state = crypto2.randomBytes(16).toString("hex");
|
|
246
|
+
const params = new URLSearchParams({
|
|
247
|
+
code: "true",
|
|
248
|
+
client_id: CLIENT_ID,
|
|
249
|
+
response_type: "code",
|
|
250
|
+
redirect_uri: REDIRECT_URI,
|
|
251
|
+
scope: SCOPES,
|
|
252
|
+
code_challenge: challenge,
|
|
253
|
+
code_challenge_method: "S256",
|
|
254
|
+
state
|
|
255
|
+
});
|
|
256
|
+
const authUrl = `${AUTHORIZE_URL}?${params}`;
|
|
257
|
+
callbacks.onOpenUrl(authUrl);
|
|
258
|
+
const raw = await callbacks.onPromptCode("Paste the code from the browser (format: code#state):");
|
|
259
|
+
const parts = raw.trim().split("#");
|
|
260
|
+
if (parts.length !== 2 || !parts[0] || parts[1] !== state) {
|
|
261
|
+
throw new Error("Invalid code or state mismatch. Please try again.");
|
|
262
|
+
}
|
|
263
|
+
return exchangeAnthropicCode(parts[0], parts[1], verifier);
|
|
264
|
+
}
|
|
265
|
+
async function exchangeAnthropicCode(code, state, verifier) {
|
|
266
|
+
const data = await postTokenRequest(
|
|
267
|
+
{
|
|
268
|
+
grant_type: "authorization_code",
|
|
269
|
+
client_id: CLIENT_ID,
|
|
270
|
+
code,
|
|
271
|
+
state,
|
|
272
|
+
redirect_uri: REDIRECT_URI,
|
|
273
|
+
code_verifier: verifier
|
|
274
|
+
},
|
|
275
|
+
"token exchange"
|
|
276
|
+
);
|
|
277
|
+
return toCredentials(data);
|
|
278
|
+
}
|
|
279
|
+
async function refreshAnthropicToken(refreshToken) {
|
|
280
|
+
const data = await postTokenRequest(
|
|
281
|
+
{
|
|
282
|
+
grant_type: "refresh_token",
|
|
283
|
+
client_id: CLIENT_ID,
|
|
284
|
+
refresh_token: refreshToken
|
|
285
|
+
},
|
|
286
|
+
"token refresh"
|
|
287
|
+
);
|
|
288
|
+
return toCredentials(data);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/oauth/openai.ts
|
|
292
|
+
import http from "http";
|
|
293
|
+
import crypto3 from "crypto";
|
|
294
|
+
var CLIENT_ID2 = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
295
|
+
var AUTHORIZE_URL2 = "https://auth.openai.com/oauth/authorize";
|
|
296
|
+
var TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
297
|
+
var REDIRECT_URI2 = "http://localhost:1455/auth/callback";
|
|
298
|
+
var SCOPE = "openid profile email offline_access api.connectors.read api.connectors.invoke";
|
|
299
|
+
var JWT_CLAIM_PATH = "https://api.openai.com/auth";
|
|
300
|
+
async function loginOpenAI(callbacks) {
|
|
301
|
+
const { verifier, challenge } = await generatePKCE();
|
|
302
|
+
const state = crypto3.randomBytes(16).toString("hex");
|
|
303
|
+
const url = new URL(AUTHORIZE_URL2);
|
|
304
|
+
url.searchParams.set("response_type", "code");
|
|
305
|
+
url.searchParams.set("client_id", CLIENT_ID2);
|
|
306
|
+
url.searchParams.set("redirect_uri", REDIRECT_URI2);
|
|
307
|
+
url.searchParams.set("scope", SCOPE);
|
|
308
|
+
url.searchParams.set("code_challenge", challenge);
|
|
309
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
310
|
+
url.searchParams.set("state", state);
|
|
311
|
+
url.searchParams.set("prompt", "login");
|
|
312
|
+
url.searchParams.set("id_token_add_organizations", "true");
|
|
313
|
+
url.searchParams.set("codex_cli_simplified_flow", "true");
|
|
314
|
+
url.searchParams.set("originator", "ggcoder");
|
|
315
|
+
let code;
|
|
316
|
+
try {
|
|
317
|
+
code = await loginWithServer(url.toString(), state, callbacks);
|
|
318
|
+
} catch {
|
|
319
|
+
callbacks.onOpenUrl(url.toString());
|
|
320
|
+
const raw = await callbacks.onPromptCode(
|
|
321
|
+
"Could not start local server. Paste the callback URL or code from the browser:"
|
|
322
|
+
);
|
|
323
|
+
const parsed = parseAuthorizationInput(raw);
|
|
324
|
+
if (!parsed.code) {
|
|
325
|
+
throw new Error("No authorization code found in input.");
|
|
326
|
+
}
|
|
327
|
+
code = parsed.code;
|
|
328
|
+
}
|
|
329
|
+
const creds = await exchangeOpenAICode(code, verifier);
|
|
330
|
+
const accountId = getAccountId(creds.accessToken);
|
|
331
|
+
if (!accountId) {
|
|
332
|
+
throw new Error("Failed to extract accountId from OpenAI token.");
|
|
333
|
+
}
|
|
334
|
+
creds.accountId = accountId;
|
|
335
|
+
return creds;
|
|
336
|
+
}
|
|
337
|
+
function parseAuthorizationInput(input) {
|
|
338
|
+
const value = input.trim();
|
|
339
|
+
if (!value) return {};
|
|
340
|
+
try {
|
|
341
|
+
const url = new URL(value);
|
|
342
|
+
return {
|
|
343
|
+
code: url.searchParams.get("code") ?? void 0,
|
|
344
|
+
state: url.searchParams.get("state") ?? void 0
|
|
345
|
+
};
|
|
346
|
+
} catch {
|
|
347
|
+
}
|
|
348
|
+
if (value.includes("#")) {
|
|
349
|
+
const [code, state] = value.split("#", 2);
|
|
350
|
+
return { code, state };
|
|
351
|
+
}
|
|
352
|
+
if (value.includes("code=")) {
|
|
353
|
+
const params = new URLSearchParams(value);
|
|
354
|
+
return {
|
|
355
|
+
code: params.get("code") ?? void 0,
|
|
356
|
+
state: params.get("state") ?? void 0
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return { code: value };
|
|
360
|
+
}
|
|
361
|
+
function decodeJwt(token) {
|
|
362
|
+
try {
|
|
363
|
+
const parts = token.split(".");
|
|
364
|
+
if (parts.length !== 3) return null;
|
|
365
|
+
const decoded = atob(parts[1]);
|
|
366
|
+
return JSON.parse(decoded);
|
|
367
|
+
} catch {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function getAccountId(accessToken) {
|
|
372
|
+
const payload = decodeJwt(accessToken);
|
|
373
|
+
const auth = payload?.[JWT_CLAIM_PATH];
|
|
374
|
+
const accountId = auth?.chatgpt_account_id;
|
|
375
|
+
return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
|
|
376
|
+
}
|
|
377
|
+
async function loginWithServer(authUrl, expectedState, callbacks) {
|
|
378
|
+
return new Promise((resolve, reject) => {
|
|
379
|
+
let receivedCode = null;
|
|
380
|
+
const server = http.createServer((req, res) => {
|
|
381
|
+
const url = new URL(req.url || "", "http://localhost");
|
|
382
|
+
if (url.pathname !== "/auth/callback") {
|
|
383
|
+
res.statusCode = 404;
|
|
384
|
+
res.end("Not found");
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (url.searchParams.get("state") !== expectedState) {
|
|
388
|
+
res.statusCode = 400;
|
|
389
|
+
res.end("State mismatch");
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
receivedCode = url.searchParams.get("code");
|
|
393
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
394
|
+
res.end("<html><body><h1>Login successful!</h1><p>You can close this tab.</p></body></html>");
|
|
395
|
+
server.close();
|
|
396
|
+
});
|
|
397
|
+
server.on("error", (err) => {
|
|
398
|
+
reject(err);
|
|
399
|
+
});
|
|
400
|
+
server.listen(1455, "127.0.0.1", () => {
|
|
401
|
+
callbacks.onOpenUrl(authUrl);
|
|
402
|
+
callbacks.onStatus("Waiting for browser callback...");
|
|
403
|
+
});
|
|
404
|
+
const timeout = setTimeout(() => {
|
|
405
|
+
if (!receivedCode) {
|
|
406
|
+
server.close();
|
|
407
|
+
}
|
|
408
|
+
}, 12e4);
|
|
409
|
+
timeout.unref();
|
|
410
|
+
server.on("close", () => {
|
|
411
|
+
clearTimeout(timeout);
|
|
412
|
+
if (receivedCode) {
|
|
413
|
+
resolve(receivedCode);
|
|
414
|
+
} else {
|
|
415
|
+
reject(new Error("Server closed without receiving code"));
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
async function exchangeOpenAICode(code, verifier) {
|
|
421
|
+
const response = await fetch(TOKEN_URL, {
|
|
422
|
+
method: "POST",
|
|
423
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
424
|
+
body: new URLSearchParams({
|
|
425
|
+
grant_type: "authorization_code",
|
|
426
|
+
client_id: CLIENT_ID2,
|
|
427
|
+
code,
|
|
428
|
+
redirect_uri: REDIRECT_URI2,
|
|
429
|
+
code_verifier: verifier
|
|
430
|
+
})
|
|
431
|
+
});
|
|
432
|
+
if (!response.ok) {
|
|
433
|
+
const text = await response.text();
|
|
434
|
+
throw new Error(`OpenAI token exchange failed (${response.status}): ${text}`);
|
|
435
|
+
}
|
|
436
|
+
const data = await response.json();
|
|
437
|
+
return {
|
|
438
|
+
accessToken: data.access_token,
|
|
439
|
+
refreshToken: data.refresh_token,
|
|
440
|
+
expiresAt: Date.now() + data.expires_in * 1e3
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
async function refreshOpenAIToken(refreshToken) {
|
|
444
|
+
const response = await fetch(TOKEN_URL, {
|
|
445
|
+
method: "POST",
|
|
446
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
447
|
+
body: new URLSearchParams({
|
|
448
|
+
grant_type: "refresh_token",
|
|
449
|
+
refresh_token: refreshToken,
|
|
450
|
+
client_id: CLIENT_ID2
|
|
451
|
+
})
|
|
452
|
+
});
|
|
453
|
+
if (!response.ok) {
|
|
454
|
+
const text = await response.text();
|
|
455
|
+
throw new Error(`OpenAI token refresh failed (${response.status}): ${text}`);
|
|
456
|
+
}
|
|
457
|
+
const data = await response.json();
|
|
458
|
+
const creds = {
|
|
459
|
+
accessToken: data.access_token,
|
|
460
|
+
refreshToken: data.refresh_token,
|
|
461
|
+
expiresAt: Date.now() + data.expires_in * 1e3
|
|
462
|
+
};
|
|
463
|
+
const accountId = getAccountId(creds.accessToken);
|
|
464
|
+
if (accountId) {
|
|
465
|
+
creds.accountId = accountId;
|
|
466
|
+
}
|
|
467
|
+
return creds;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/oauth/gemini.ts
|
|
471
|
+
import http2 from "http";
|
|
472
|
+
import crypto4 from "crypto";
|
|
473
|
+
var CLIENT_ID_ENV = "GGCODER_GEMINI_OAUTH_CLIENT_ID";
|
|
474
|
+
var CLIENT_SECRET_ENV = "GGCODER_GEMINI_OAUTH_CLIENT_SECRET";
|
|
475
|
+
var DEFAULT_CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
|
|
476
|
+
var DEFAULT_CLIENT_SECRET = "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";
|
|
477
|
+
var AUTHORIZE_URL3 = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
478
|
+
var TOKEN_URL2 = "https://oauth2.googleapis.com/token";
|
|
479
|
+
var CODE_ASSIST_BASE_URL = "https://cloudcode-pa.googleapis.com";
|
|
480
|
+
var CODE_ASSIST_API_VERSION = "v1internal";
|
|
481
|
+
var CODE_ASSIST_POST_RETRIES = 3;
|
|
482
|
+
var CODE_ASSIST_POST_RETRY_DELAY_MS = 100;
|
|
483
|
+
var SCOPE2 = [
|
|
484
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
485
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
486
|
+
"https://www.googleapis.com/auth/userinfo.profile"
|
|
487
|
+
].join(" ");
|
|
488
|
+
var USER_TIER_FREE = "free-tier";
|
|
489
|
+
var USER_TIER_LEGACY = "legacy-tier";
|
|
490
|
+
var USER_TIER_STANDARD = "standard-tier";
|
|
491
|
+
var VALIDATION_REQUIRED_REASON = "VALIDATION_REQUIRED";
|
|
492
|
+
var VPC_SC_REASON = "SECURITY_POLICY_VIOLATED";
|
|
493
|
+
var CodeAssistHttpError = class extends Error {
|
|
494
|
+
status;
|
|
495
|
+
body;
|
|
496
|
+
constructor(label, status, body) {
|
|
497
|
+
super(`Gemini Code Assist ${label} failed (${status}): ${body}`);
|
|
498
|
+
this.name = "CodeAssistHttpError";
|
|
499
|
+
this.status = status;
|
|
500
|
+
this.body = body;
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
async function loginGemini(callbacks) {
|
|
504
|
+
const { clientId, clientSecret } = getGeminiOAuthClientCredentials();
|
|
505
|
+
const { verifier, challenge } = await generatePKCE();
|
|
506
|
+
const state = crypto4.randomBytes(32).toString("hex");
|
|
507
|
+
const redirectUri = await getLoopbackRedirectUri();
|
|
508
|
+
const url = new URL(AUTHORIZE_URL3);
|
|
509
|
+
url.searchParams.set("response_type", "code");
|
|
510
|
+
url.searchParams.set("client_id", clientId);
|
|
511
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
512
|
+
url.searchParams.set("scope", SCOPE2);
|
|
513
|
+
url.searchParams.set("access_type", "offline");
|
|
514
|
+
url.searchParams.set("prompt", "consent");
|
|
515
|
+
url.searchParams.set("code_challenge", challenge);
|
|
516
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
517
|
+
url.searchParams.set("state", state);
|
|
518
|
+
let code;
|
|
519
|
+
try {
|
|
520
|
+
code = await loginWithServer2(url.toString(), redirectUri, state, callbacks);
|
|
521
|
+
} catch {
|
|
522
|
+
callbacks.onOpenUrl(url.toString());
|
|
523
|
+
const raw = await callbacks.onPromptCode(
|
|
524
|
+
"Could not start local server. Paste the callback URL or code from the browser:"
|
|
525
|
+
);
|
|
526
|
+
const parsed = parseAuthorizationInput2(raw);
|
|
527
|
+
if (!parsed.code) {
|
|
528
|
+
throw new Error("No authorization code found in input.");
|
|
529
|
+
}
|
|
530
|
+
if (parsed.state && parsed.state !== state) {
|
|
531
|
+
throw new Error("Invalid state. Please try again.");
|
|
532
|
+
}
|
|
533
|
+
code = parsed.code;
|
|
534
|
+
}
|
|
535
|
+
const creds = await exchangeGeminiCode(code, verifier, redirectUri, clientId, clientSecret);
|
|
536
|
+
callbacks.onStatus("Setting up Gemini Code Assist access...");
|
|
537
|
+
const projectId = await setupCodeAssistProject(creds.accessToken, callbacks);
|
|
538
|
+
return {
|
|
539
|
+
...creds,
|
|
540
|
+
projectId
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
async function refreshGeminiToken(refreshToken) {
|
|
544
|
+
const { clientId, clientSecret } = getGeminiOAuthClientCredentials();
|
|
545
|
+
const data = await postTokenRequest2({
|
|
546
|
+
grant_type: "refresh_token",
|
|
547
|
+
refresh_token: refreshToken,
|
|
548
|
+
client_id: clientId,
|
|
549
|
+
client_secret: clientSecret
|
|
550
|
+
});
|
|
551
|
+
return {
|
|
552
|
+
accessToken: data.access_token,
|
|
553
|
+
refreshToken: data.refresh_token ?? refreshToken,
|
|
554
|
+
expiresAt: Date.now() + data.expires_in * 1e3 - 5 * 60 * 1e3
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function getGeminiOAuthClientCredentials() {
|
|
558
|
+
const clientId = process.env[CLIENT_ID_ENV]?.trim() || DEFAULT_CLIENT_ID;
|
|
559
|
+
const clientSecret = process.env[CLIENT_SECRET_ENV]?.trim() || DEFAULT_CLIENT_SECRET;
|
|
560
|
+
return { clientId, clientSecret };
|
|
561
|
+
}
|
|
562
|
+
async function getLoopbackRedirectUri() {
|
|
563
|
+
return new Promise((resolve, reject) => {
|
|
564
|
+
const server = http2.createServer();
|
|
565
|
+
server.listen(0, "127.0.0.1", () => {
|
|
566
|
+
const addr = server.address();
|
|
567
|
+
server.close(() => {
|
|
568
|
+
if (addr && typeof addr === "object") {
|
|
569
|
+
resolve(`http://127.0.0.1:${addr.port}/oauth2callback`);
|
|
570
|
+
} else {
|
|
571
|
+
reject(new Error("Failed to allocate OAuth callback port."));
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
server.on("error", reject);
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
function parseAuthorizationInput2(input) {
|
|
579
|
+
const value = input.trim();
|
|
580
|
+
if (!value) return {};
|
|
581
|
+
try {
|
|
582
|
+
const url = new URL(value);
|
|
583
|
+
return {
|
|
584
|
+
code: url.searchParams.get("code") ?? void 0,
|
|
585
|
+
state: url.searchParams.get("state") ?? void 0
|
|
586
|
+
};
|
|
587
|
+
} catch {
|
|
588
|
+
}
|
|
589
|
+
if (value.includes("code=")) {
|
|
590
|
+
const params = new URLSearchParams(value);
|
|
591
|
+
return {
|
|
592
|
+
code: params.get("code") ?? void 0,
|
|
593
|
+
state: params.get("state") ?? void 0
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
return { code: value };
|
|
597
|
+
}
|
|
598
|
+
async function loginWithServer2(authUrl, redirectUri, expectedState, callbacks) {
|
|
599
|
+
const redirect = new URL(redirectUri);
|
|
600
|
+
const port = Number(redirect.port);
|
|
601
|
+
return new Promise((resolve, reject) => {
|
|
602
|
+
let receivedCode = null;
|
|
603
|
+
const server = http2.createServer((req, res) => {
|
|
604
|
+
const url = new URL(req.url || "", redirect.origin);
|
|
605
|
+
if (url.pathname !== redirect.pathname) {
|
|
606
|
+
res.statusCode = 404;
|
|
607
|
+
res.end("Not found");
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (url.searchParams.get("state") !== expectedState) {
|
|
611
|
+
res.statusCode = 400;
|
|
612
|
+
res.end("State mismatch");
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
receivedCode = url.searchParams.get("code");
|
|
616
|
+
res.writeHead(200, { "Content-Type": "text/html", Connection: "close" });
|
|
617
|
+
res.end("<html><body><h1>Login successful!</h1><p>You can close this tab.</p></body></html>");
|
|
618
|
+
server.close();
|
|
619
|
+
});
|
|
620
|
+
server.on("error", (err) => reject(err));
|
|
621
|
+
server.listen(port, "127.0.0.1", () => {
|
|
622
|
+
callbacks.onOpenUrl(authUrl);
|
|
623
|
+
callbacks.onStatus("Waiting for browser callback...");
|
|
624
|
+
});
|
|
625
|
+
const timeout = setTimeout(() => {
|
|
626
|
+
if (!receivedCode) server.close();
|
|
627
|
+
}, 12e4);
|
|
628
|
+
timeout.unref();
|
|
629
|
+
server.on("close", () => {
|
|
630
|
+
clearTimeout(timeout);
|
|
631
|
+
if (receivedCode) {
|
|
632
|
+
resolve(receivedCode);
|
|
633
|
+
} else {
|
|
634
|
+
reject(new Error("Server closed without receiving code."));
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
async function exchangeGeminiCode(code, verifier, redirectUri, clientId, clientSecret) {
|
|
640
|
+
const data = await postTokenRequest2({
|
|
641
|
+
grant_type: "authorization_code",
|
|
642
|
+
client_id: clientId,
|
|
643
|
+
client_secret: clientSecret,
|
|
644
|
+
code,
|
|
645
|
+
redirect_uri: redirectUri,
|
|
646
|
+
code_verifier: verifier
|
|
647
|
+
});
|
|
648
|
+
if (!data.refresh_token) {
|
|
649
|
+
throw new Error("Gemini OAuth did not return a refresh token. Please try login again.");
|
|
650
|
+
}
|
|
651
|
+
return {
|
|
652
|
+
accessToken: data.access_token,
|
|
653
|
+
refreshToken: data.refresh_token,
|
|
654
|
+
expiresAt: Date.now() + data.expires_in * 1e3 - 5 * 60 * 1e3
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
async function postTokenRequest2(body) {
|
|
658
|
+
const response = await fetch(TOKEN_URL2, {
|
|
659
|
+
method: "POST",
|
|
660
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
661
|
+
body: new URLSearchParams(body)
|
|
662
|
+
});
|
|
663
|
+
if (!response.ok) {
|
|
664
|
+
const text = await response.text();
|
|
665
|
+
throw new Error(`Gemini token request failed (${response.status}): ${text}`);
|
|
666
|
+
}
|
|
667
|
+
return await response.json();
|
|
668
|
+
}
|
|
669
|
+
async function setupCodeAssistProject(accessToken, callbacks) {
|
|
670
|
+
const envProject = process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GOOGLE_CLOUD_PROJECT_ID;
|
|
671
|
+
if (envProject && /^\d+$/.test(envProject)) {
|
|
672
|
+
throw new Error("GOOGLE_CLOUD_PROJECT must be a project ID, not a numeric project number.");
|
|
673
|
+
}
|
|
674
|
+
const coreMetadata = {
|
|
675
|
+
ideType: "IDE_UNSPECIFIED",
|
|
676
|
+
platform: "PLATFORM_UNSPECIFIED",
|
|
677
|
+
pluginType: "GEMINI"
|
|
678
|
+
};
|
|
679
|
+
const projectMetadata = {
|
|
680
|
+
...coreMetadata,
|
|
681
|
+
...envProject ? { duetProject: envProject } : {}
|
|
682
|
+
};
|
|
683
|
+
let loadRes;
|
|
684
|
+
while (true) {
|
|
685
|
+
loadRes = await loadCodeAssist(accessToken, envProject, projectMetadata);
|
|
686
|
+
const validation = getValidationRequiredTier(loadRes);
|
|
687
|
+
if (!validation) break;
|
|
688
|
+
callbacks.onStatus(
|
|
689
|
+
`Gemini Code Assist requires account validation${validation.reasonMessage ? `: ${validation.reasonMessage}` : ""}`
|
|
690
|
+
);
|
|
691
|
+
callbacks.onOpenUrl(validation.validationUrl);
|
|
692
|
+
const answer = await callbacks.onPromptCode(
|
|
693
|
+
"Complete validation in the browser, then press Enter to retry (or type cancel):"
|
|
694
|
+
);
|
|
695
|
+
if (answer.trim().toLowerCase() === "cancel") {
|
|
696
|
+
throw new Error("Gemini Code Assist account validation was cancelled.");
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (loadRes.currentTier) {
|
|
700
|
+
const project2 = loadRes.cloudaicompanionProject ?? envProject;
|
|
701
|
+
if (!project2) throwProjectError(loadRes);
|
|
702
|
+
return project2;
|
|
703
|
+
}
|
|
704
|
+
const tier = getOnboardTier(loadRes);
|
|
705
|
+
const onboardReq = tier.id === USER_TIER_FREE ? {
|
|
706
|
+
tierId: tier.id,
|
|
707
|
+
cloudaicompanionProject: void 0,
|
|
708
|
+
metadata: coreMetadata
|
|
709
|
+
} : {
|
|
710
|
+
tierId: tier.id,
|
|
711
|
+
cloudaicompanionProject: envProject,
|
|
712
|
+
metadata: projectMetadata
|
|
713
|
+
};
|
|
714
|
+
let operation = await codeAssistPost(
|
|
715
|
+
accessToken,
|
|
716
|
+
"onboardUser",
|
|
717
|
+
onboardReq
|
|
718
|
+
);
|
|
719
|
+
while (!operation.done && operation.name) {
|
|
720
|
+
await new Promise((resolve) => setTimeout(resolve, 5e3));
|
|
721
|
+
operation = await codeAssistGet(accessToken, operation.name);
|
|
722
|
+
}
|
|
723
|
+
const project = operation.response?.cloudaicompanionProject?.id ?? envProject;
|
|
724
|
+
if (!project) throwProjectError(loadRes);
|
|
725
|
+
return project;
|
|
726
|
+
}
|
|
727
|
+
async function loadCodeAssist(accessToken, envProject, metadata) {
|
|
728
|
+
try {
|
|
729
|
+
return await codeAssistPost(accessToken, "loadCodeAssist", {
|
|
730
|
+
...envProject ? { cloudaicompanionProject: envProject } : {},
|
|
731
|
+
metadata
|
|
732
|
+
});
|
|
733
|
+
} catch (err) {
|
|
734
|
+
if (err instanceof CodeAssistHttpError && isVpcScAffectedError(err)) {
|
|
735
|
+
return { currentTier: { id: USER_TIER_STANDARD } };
|
|
736
|
+
}
|
|
737
|
+
if (err instanceof CodeAssistHttpError && err.status === 403 && envProject === "cloudshell-gca") {
|
|
738
|
+
throw new Error(
|
|
739
|
+
"Access to the default Cloud Shell Gemini project was denied.\nPlease set your own Google Cloud project by running:\ngcloud config set project [PROJECT_ID]\nor setting export GOOGLE_CLOUD_PROJECT=...",
|
|
740
|
+
{ cause: err }
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
throw err;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
function getValidationRequiredTier(response) {
|
|
747
|
+
return response.ineligibleTiers?.find(
|
|
748
|
+
(tier) => tier.reasonCode === VALIDATION_REQUIRED_REASON && typeof tier.validationUrl === "string"
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
function getOnboardTier(response) {
|
|
752
|
+
const defaultTier = response.allowedTiers?.find((tier) => tier.isDefault);
|
|
753
|
+
return defaultTier ?? { id: USER_TIER_LEGACY, name: "" };
|
|
754
|
+
}
|
|
755
|
+
function throwProjectError(response) {
|
|
756
|
+
const reasons = response.ineligibleTiers?.map((tier) => tier.reasonMessage ?? tier.tierName).filter((reason) => Boolean(reason));
|
|
757
|
+
if (reasons && reasons.length > 0) {
|
|
758
|
+
throw new Error(`Gemini Code Assist setup failed: ${reasons.join(", ")}`);
|
|
759
|
+
}
|
|
760
|
+
throw new Error(
|
|
761
|
+
"Gemini requires a Google Cloud project for this account. Set GOOGLE_CLOUD_PROJECT and try again."
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
async function codeAssistPost(accessToken, method, body) {
|
|
765
|
+
let lastError;
|
|
766
|
+
for (let attempt = 0; attempt <= CODE_ASSIST_POST_RETRIES; attempt++) {
|
|
767
|
+
try {
|
|
768
|
+
return await codeAssistRequest(getCodeAssistMethodUrl(method), accessToken, method, {
|
|
769
|
+
method: "POST",
|
|
770
|
+
body: JSON.stringify(body)
|
|
771
|
+
});
|
|
772
|
+
} catch (err) {
|
|
773
|
+
if (!(err instanceof CodeAssistHttpError) || attempt === CODE_ASSIST_POST_RETRIES || !shouldRetryCodeAssistStatus(err.status)) {
|
|
774
|
+
throw err;
|
|
775
|
+
}
|
|
776
|
+
lastError = err;
|
|
777
|
+
}
|
|
778
|
+
await new Promise((resolve) => setTimeout(resolve, CODE_ASSIST_POST_RETRY_DELAY_MS));
|
|
779
|
+
}
|
|
780
|
+
throw lastError ?? new Error(`Gemini Code Assist ${method} failed.`);
|
|
781
|
+
}
|
|
782
|
+
async function codeAssistGet(accessToken, operationName) {
|
|
783
|
+
return codeAssistRequest(getCodeAssistOperationUrl(operationName), accessToken, "operation", {
|
|
784
|
+
method: "GET"
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
async function codeAssistRequest(url, accessToken, label, init) {
|
|
788
|
+
const response = await fetch(url, {
|
|
789
|
+
...init,
|
|
790
|
+
headers: codeAssistHeaders(accessToken)
|
|
791
|
+
});
|
|
792
|
+
if (!response.ok) {
|
|
793
|
+
const text = await response.text();
|
|
794
|
+
throw new CodeAssistHttpError(label, response.status, text);
|
|
795
|
+
}
|
|
796
|
+
return await response.json();
|
|
797
|
+
}
|
|
798
|
+
function getCodeAssistBaseUrl() {
|
|
799
|
+
const endpoint = process.env.CODE_ASSIST_ENDPOINT ?? CODE_ASSIST_BASE_URL;
|
|
800
|
+
const version = process.env.CODE_ASSIST_API_VERSION || CODE_ASSIST_API_VERSION;
|
|
801
|
+
return `${endpoint}/${version}`;
|
|
802
|
+
}
|
|
803
|
+
function getCodeAssistMethodUrl(method) {
|
|
804
|
+
return `${getCodeAssistBaseUrl()}:${method}`;
|
|
805
|
+
}
|
|
806
|
+
function getCodeAssistOperationUrl(operationName) {
|
|
807
|
+
return `${getCodeAssistBaseUrl()}/${operationName}`;
|
|
808
|
+
}
|
|
809
|
+
function shouldRetryCodeAssistStatus(status) {
|
|
810
|
+
return status === 429 || status === 499 || status >= 500 && status <= 599;
|
|
811
|
+
}
|
|
812
|
+
function isVpcScAffectedError(error) {
|
|
813
|
+
try {
|
|
814
|
+
const parsed = JSON.parse(error.body);
|
|
815
|
+
if (!parsed || typeof parsed !== "object" || !("error" in parsed)) return false;
|
|
816
|
+
const details = parsed.error?.details;
|
|
817
|
+
return Array.isArray(details) ? details.some(
|
|
818
|
+
(detail) => detail != null && typeof detail === "object" && "reason" in detail && detail.reason === VPC_SC_REASON
|
|
819
|
+
) : false;
|
|
820
|
+
} catch {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function codeAssistHeaders(accessToken) {
|
|
825
|
+
return {
|
|
826
|
+
Authorization: `Bearer ${accessToken}`,
|
|
827
|
+
"Content-Type": "application/json",
|
|
828
|
+
"User-Agent": "google-gemini-cli",
|
|
829
|
+
"X-Goog-Api-Client": "gemini-cli/0.0.0"
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/oauth/kimi.ts
|
|
834
|
+
import { execFileSync } from "child_process";
|
|
835
|
+
import { randomUUID } from "crypto";
|
|
836
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
837
|
+
import { arch, hostname, release, type } from "os";
|
|
838
|
+
import path3 from "path";
|
|
839
|
+
var CLIENT_ID3 = "17e5f671-d194-4dfb-9706-5516cb48c098";
|
|
840
|
+
var DEFAULT_OAUTH_HOST = "https://auth.kimi.com";
|
|
841
|
+
var DEFAULT_CODING_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
842
|
+
var KIMI_PLATFORM = "kimi_code_cli";
|
|
843
|
+
var DEFAULT_KIMI_VERSION = "1.0.11";
|
|
844
|
+
var DEVICE_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
845
|
+
function oauthHost() {
|
|
846
|
+
const host = process.env.KIMI_CODE_OAUTH_HOST ?? process.env.KIMI_OAUTH_HOST ?? DEFAULT_OAUTH_HOST;
|
|
847
|
+
return host.replace(/\/+$/, "");
|
|
848
|
+
}
|
|
849
|
+
function kimiCodeBaseUrl() {
|
|
850
|
+
return (process.env.KIMI_CODE_BASE_URL ?? DEFAULT_CODING_BASE_URL).replace(/\/+$/, "");
|
|
851
|
+
}
|
|
852
|
+
function kimiVersion() {
|
|
853
|
+
const v = process.env.KIMI_CODE_VERSION ?? DEFAULT_KIMI_VERSION;
|
|
854
|
+
return asciiHeader(v, DEFAULT_KIMI_VERSION);
|
|
855
|
+
}
|
|
856
|
+
function asciiHeader(value, fallback = "unknown") {
|
|
857
|
+
const cleaned = value.replace(/[^\u0020-\u007E]/g, "").trim();
|
|
858
|
+
return cleaned.length > 0 ? cleaned : fallback;
|
|
859
|
+
}
|
|
860
|
+
function macOsProductVersion() {
|
|
861
|
+
try {
|
|
862
|
+
const version = execFileSync("/usr/bin/sw_vers", ["-productVersion"], {
|
|
863
|
+
encoding: "utf-8",
|
|
864
|
+
timeout: 1e3
|
|
865
|
+
}).trim();
|
|
866
|
+
return version.length > 0 ? version : void 0;
|
|
867
|
+
} catch {
|
|
868
|
+
return void 0;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
function deviceModel() {
|
|
872
|
+
const os = type();
|
|
873
|
+
const version = release();
|
|
874
|
+
const osArch = arch();
|
|
875
|
+
if (os === "Darwin") return `macOS ${macOsProductVersion() ?? version} ${osArch}`;
|
|
876
|
+
if (os === "Windows_NT") return `Windows ${version} ${osArch}`;
|
|
877
|
+
return `${os} ${version} ${osArch}`.trim();
|
|
878
|
+
}
|
|
879
|
+
function deviceId() {
|
|
880
|
+
const idPath = path3.join(getAppPaths().agentDir, "kimi_device_id");
|
|
881
|
+
if (existsSync(idPath)) {
|
|
882
|
+
try {
|
|
883
|
+
const text = readFileSync(idPath, "utf-8").trim();
|
|
884
|
+
if (text.length > 0) return text;
|
|
885
|
+
} catch {
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
const id = randomUUID();
|
|
889
|
+
try {
|
|
890
|
+
mkdirSync(getAppPaths().agentDir, { recursive: true, mode: 448 });
|
|
891
|
+
writeFileSync(idPath, id, { encoding: "utf-8", mode: 384 });
|
|
892
|
+
} catch {
|
|
893
|
+
}
|
|
894
|
+
return id;
|
|
895
|
+
}
|
|
896
|
+
function deviceHeaders() {
|
|
897
|
+
return {
|
|
898
|
+
"X-Msh-Platform": KIMI_PLATFORM,
|
|
899
|
+
"X-Msh-Version": kimiVersion(),
|
|
900
|
+
"X-Msh-Device-Name": asciiHeader(hostname()),
|
|
901
|
+
"X-Msh-Device-Model": asciiHeader(deviceModel()),
|
|
902
|
+
"X-Msh-Os-Version": asciiHeader(release()),
|
|
903
|
+
"X-Msh-Device-Id": deviceId()
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
function kimiCodingHeaders() {
|
|
907
|
+
return {
|
|
908
|
+
"User-Agent": `kimi-code-cli/${kimiVersion()}`,
|
|
909
|
+
...deviceHeaders()
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
function isKimiCodingEndpoint(baseUrl) {
|
|
913
|
+
if (typeof baseUrl !== "string" || baseUrl.length === 0) return false;
|
|
914
|
+
const normalized = baseUrl.replace(/\/+$/, "");
|
|
915
|
+
return normalized === kimiCodeBaseUrl() || /(^|\.)kimi\.com/i.test(normalized);
|
|
916
|
+
}
|
|
917
|
+
async function postForm(endpoint, params) {
|
|
918
|
+
const response = await fetch(`${oauthHost()}${endpoint}`, {
|
|
919
|
+
method: "POST",
|
|
920
|
+
headers: {
|
|
921
|
+
...deviceHeaders(),
|
|
922
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
923
|
+
Accept: "application/json"
|
|
924
|
+
},
|
|
925
|
+
body: new URLSearchParams(params).toString()
|
|
926
|
+
});
|
|
927
|
+
let data = {};
|
|
928
|
+
try {
|
|
929
|
+
const parsed = await response.json();
|
|
930
|
+
if (parsed && typeof parsed === "object") data = parsed;
|
|
931
|
+
} catch {
|
|
932
|
+
}
|
|
933
|
+
return { status: response.status, data };
|
|
934
|
+
}
|
|
935
|
+
function errorDetail(data) {
|
|
936
|
+
const desc = data.error_description ?? data.message ?? data.error;
|
|
937
|
+
return typeof desc === "string" && desc.length > 0 ? desc : "unknown error";
|
|
938
|
+
}
|
|
939
|
+
function credsFromTokenResponse(data, opts) {
|
|
940
|
+
const accessToken = data.access_token;
|
|
941
|
+
const responseRefreshToken = data.refresh_token;
|
|
942
|
+
const expiresIn = Number(data.expires_in);
|
|
943
|
+
if (typeof accessToken !== "string" || accessToken.length === 0) {
|
|
944
|
+
throw new Error("Kimi OAuth response missing access_token.");
|
|
945
|
+
}
|
|
946
|
+
const refreshToken = typeof responseRefreshToken === "string" && responseRefreshToken.length > 0 ? responseRefreshToken : opts?.fallbackRefreshToken ?? "";
|
|
947
|
+
if (refreshToken.length === 0) {
|
|
948
|
+
throw new Error("Kimi OAuth response missing refresh_token.");
|
|
949
|
+
}
|
|
950
|
+
if (!Number.isFinite(expiresIn) || expiresIn <= 0) {
|
|
951
|
+
throw new Error("Kimi OAuth response missing or invalid expires_in.");
|
|
952
|
+
}
|
|
953
|
+
return {
|
|
954
|
+
accessToken,
|
|
955
|
+
refreshToken,
|
|
956
|
+
expiresAt: Date.now() + expiresIn * 1e3,
|
|
957
|
+
baseUrl: kimiCodeBaseUrl()
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
async function requestDeviceAuthorization() {
|
|
961
|
+
const { status, data } = await postForm("/api/oauth/device_authorization", {
|
|
962
|
+
client_id: CLIENT_ID3
|
|
963
|
+
});
|
|
964
|
+
if (status !== 200) {
|
|
965
|
+
throw new Error(`Kimi device authorization failed (${status}): ${errorDetail(data)}`);
|
|
966
|
+
}
|
|
967
|
+
const userCode = data.user_code;
|
|
968
|
+
const deviceCode = data.device_code;
|
|
969
|
+
const verificationUriComplete = data.verification_uri_complete;
|
|
970
|
+
if (typeof userCode !== "string" || typeof deviceCode !== "string") {
|
|
971
|
+
throw new Error("Kimi device authorization response missing user_code/device_code.");
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
userCode,
|
|
975
|
+
deviceCode,
|
|
976
|
+
verificationUri: typeof data.verification_uri === "string" ? data.verification_uri : "",
|
|
977
|
+
verificationUriComplete: typeof verificationUriComplete === "string" ? verificationUriComplete : "",
|
|
978
|
+
interval: Number(data.interval ?? 5) || 5
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
async function pollDeviceToken(deviceCode) {
|
|
982
|
+
const { status, data } = await postForm("/api/oauth/token", {
|
|
983
|
+
client_id: CLIENT_ID3,
|
|
984
|
+
device_code: deviceCode,
|
|
985
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
986
|
+
});
|
|
987
|
+
if (status === 200 && typeof data.access_token === "string") {
|
|
988
|
+
return { kind: "success", creds: credsFromTokenResponse(data) };
|
|
989
|
+
}
|
|
990
|
+
if (status >= 500) {
|
|
991
|
+
throw new Error(`Kimi token polling server error (${status}): ${errorDetail(data)}`);
|
|
992
|
+
}
|
|
993
|
+
const errorCode = typeof data.error === "string" ? data.error : "unknown_error";
|
|
994
|
+
switch (errorCode) {
|
|
995
|
+
case "authorization_pending":
|
|
996
|
+
return { kind: "pending" };
|
|
997
|
+
case "slow_down":
|
|
998
|
+
return { kind: "slow_down" };
|
|
999
|
+
case "expired_token":
|
|
1000
|
+
return { kind: "expired" };
|
|
1001
|
+
case "access_denied":
|
|
1002
|
+
return { kind: "denied" };
|
|
1003
|
+
default:
|
|
1004
|
+
throw new Error(`Kimi token polling failed (${status}): ${errorDetail(data)}`);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function sleep(ms) {
|
|
1008
|
+
return new Promise((resolve) => {
|
|
1009
|
+
setTimeout(resolve, ms);
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
async function loginKimi(callbacks) {
|
|
1013
|
+
const auth = await requestDeviceAuthorization();
|
|
1014
|
+
callbacks.onStatus(
|
|
1015
|
+
`Visit ${auth.verificationUri || auth.verificationUriComplete} and enter code: ${auth.userCode}`
|
|
1016
|
+
);
|
|
1017
|
+
callbacks.onOpenUrl(auth.verificationUriComplete || auth.verificationUri);
|
|
1018
|
+
callbacks.onStatus("Waiting for you to authorize in the browser...");
|
|
1019
|
+
const deadline = Date.now() + DEVICE_TIMEOUT_MS;
|
|
1020
|
+
let interval = Math.max(auth.interval, 1);
|
|
1021
|
+
while (Date.now() < deadline) {
|
|
1022
|
+
await sleep(interval * 1e3);
|
|
1023
|
+
const result = await pollDeviceToken(auth.deviceCode);
|
|
1024
|
+
if (result.kind === "success") return result.creds;
|
|
1025
|
+
if (result.kind === "denied") {
|
|
1026
|
+
throw new Error("Kimi authorization was denied.");
|
|
1027
|
+
}
|
|
1028
|
+
if (result.kind === "expired") {
|
|
1029
|
+
throw new Error("Kimi device code expired. Please run login again.");
|
|
1030
|
+
}
|
|
1031
|
+
if (result.kind === "slow_down") {
|
|
1032
|
+
interval += 5;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
throw new Error("Kimi login timed out. Please run login again.");
|
|
1036
|
+
}
|
|
1037
|
+
async function refreshKimiToken(refreshToken) {
|
|
1038
|
+
const { status, data } = await postForm("/api/oauth/token", {
|
|
1039
|
+
client_id: CLIENT_ID3,
|
|
1040
|
+
grant_type: "refresh_token",
|
|
1041
|
+
refresh_token: refreshToken
|
|
1042
|
+
});
|
|
1043
|
+
if (status === 200 && typeof data.access_token === "string") {
|
|
1044
|
+
return credsFromTokenResponse(data, { fallbackRefreshToken: refreshToken });
|
|
1045
|
+
}
|
|
1046
|
+
const errorCode = typeof data.error === "string" ? data.error : "";
|
|
1047
|
+
throw new Error(`Kimi token refresh failed (${status}): ${errorCode || errorDetail(data)}`);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// src/file-lock.ts
|
|
1051
|
+
import fs3 from "fs/promises";
|
|
1052
|
+
import { setTimeout as setTimeout2 } from "timers/promises";
|
|
1053
|
+
var STALE_TIMEOUT_MS = 1e4;
|
|
1054
|
+
var RETRY_INTERVAL_MS = 50;
|
|
1055
|
+
var MAX_WAIT_MS = 5e3;
|
|
1056
|
+
async function withFileLock(filePath, fn) {
|
|
1057
|
+
const lockPath = filePath + ".lock";
|
|
1058
|
+
await acquireLock(lockPath);
|
|
1059
|
+
try {
|
|
1060
|
+
return await fn();
|
|
1061
|
+
} finally {
|
|
1062
|
+
await releaseLock(lockPath);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
async function acquireLock(lockPath) {
|
|
1066
|
+
const startTime = Date.now();
|
|
1067
|
+
while (true) {
|
|
1068
|
+
try {
|
|
1069
|
+
const info = { pid: process.pid, timestamp: Date.now() };
|
|
1070
|
+
await fs3.writeFile(lockPath, JSON.stringify(info), { flag: "wx" });
|
|
1071
|
+
return;
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
if (err.code !== "EEXIST") throw err;
|
|
1074
|
+
try {
|
|
1075
|
+
const content = await fs3.readFile(lockPath, "utf-8");
|
|
1076
|
+
const info = JSON.parse(content);
|
|
1077
|
+
const isProcessAlive = isAlive(info.pid);
|
|
1078
|
+
const isStale = Date.now() - info.timestamp > STALE_TIMEOUT_MS;
|
|
1079
|
+
if (!isProcessAlive || isStale) {
|
|
1080
|
+
await fs3.unlink(lockPath).catch(() => {
|
|
1081
|
+
});
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
} catch {
|
|
1085
|
+
await fs3.unlink(lockPath).catch(() => {
|
|
1086
|
+
});
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
if (Date.now() - startTime > MAX_WAIT_MS) {
|
|
1090
|
+
await fs3.unlink(lockPath).catch(() => {
|
|
1091
|
+
});
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
await setTimeout2(RETRY_INTERVAL_MS);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
async function releaseLock(lockPath) {
|
|
1099
|
+
await fs3.unlink(lockPath).catch(() => {
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
function isAlive(pid) {
|
|
1103
|
+
try {
|
|
1104
|
+
process.kill(pid, 0);
|
|
1105
|
+
return true;
|
|
1106
|
+
} catch (err) {
|
|
1107
|
+
if (err.code === "EPERM") return true;
|
|
1108
|
+
return false;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// src/auth-storage.ts
|
|
1113
|
+
var MOONSHOT_OAUTH_KEY = "moonshot-oauth";
|
|
1114
|
+
var XIAOMI_CREDITS_KEY = "xiaomi-credits";
|
|
1115
|
+
var REFRESH_SKEW_MS = 6e4;
|
|
1116
|
+
var STATIC_API_KEY_PROVIDERS = /* @__PURE__ */ new Set([
|
|
1117
|
+
"glm",
|
|
1118
|
+
"moonshot",
|
|
1119
|
+
"xiaomi",
|
|
1120
|
+
"minimax",
|
|
1121
|
+
"deepseek",
|
|
1122
|
+
"openrouter",
|
|
1123
|
+
"sakana"
|
|
1124
|
+
]);
|
|
1125
|
+
var AuthStorage = class {
|
|
1126
|
+
data = {};
|
|
1127
|
+
filePath;
|
|
1128
|
+
loaded = false;
|
|
1129
|
+
/** Per-provider lock to serialize concurrent refresh calls. */
|
|
1130
|
+
refreshLocks = /* @__PURE__ */ new Map();
|
|
1131
|
+
constructor(filePath) {
|
|
1132
|
+
this.filePath = filePath ?? getAppPaths().authFile;
|
|
1133
|
+
}
|
|
1134
|
+
/** Path to the on-disk auth file. Useful for status output. */
|
|
1135
|
+
get path() {
|
|
1136
|
+
return this.filePath;
|
|
1137
|
+
}
|
|
1138
|
+
/** List provider keys with stored credentials. */
|
|
1139
|
+
async listProviders() {
|
|
1140
|
+
await this.ensureLoaded();
|
|
1141
|
+
return Object.keys(this.data);
|
|
1142
|
+
}
|
|
1143
|
+
/** True if credentials exist for `provider`. */
|
|
1144
|
+
async hasCredentials(provider) {
|
|
1145
|
+
await this.ensureLoaded();
|
|
1146
|
+
return Boolean(this.data[provider]);
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* First key in `keys` (in order) that has stored credentials, or `undefined`
|
|
1150
|
+
* if none do. Mirrors the first-match logic `resolveCredentials({ storageKeys })`
|
|
1151
|
+
* uses internally — callers that need to know WHICH credential will actually
|
|
1152
|
+
* be used (e.g. to clear the right one after a 401) call this directly
|
|
1153
|
+
* instead of re-deriving the same order.
|
|
1154
|
+
*/
|
|
1155
|
+
async pickStorageKey(keys) {
|
|
1156
|
+
await this.ensureLoaded();
|
|
1157
|
+
return keys.find((key) => Boolean(this.data[key]));
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* True if the user has any usable auth for the logical provider. For
|
|
1161
|
+
* `moonshot` this is satisfied by either the Kimi OAuth credential or the
|
|
1162
|
+
* Moonshot API key.
|
|
1163
|
+
*/
|
|
1164
|
+
async hasProviderAuth(provider) {
|
|
1165
|
+
await this.ensureLoaded();
|
|
1166
|
+
if (provider === "moonshot") {
|
|
1167
|
+
return Boolean(this.data[MOONSHOT_OAUTH_KEY] || this.data["moonshot"]);
|
|
1168
|
+
}
|
|
1169
|
+
if (provider === "xiaomi") {
|
|
1170
|
+
return Boolean(this.data["xiaomi"] || this.data[XIAOMI_CREDITS_KEY]);
|
|
1171
|
+
}
|
|
1172
|
+
return Boolean(this.data[provider]);
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* True if the active credential for `provider` is a static API key with no
|
|
1176
|
+
* refresh mechanism. For `moonshot` this is only true when the Kimi OAuth
|
|
1177
|
+
* credential is absent (a present OAuth credential is refreshable).
|
|
1178
|
+
*/
|
|
1179
|
+
async isStaticApiKey(provider) {
|
|
1180
|
+
await this.ensureLoaded();
|
|
1181
|
+
if (provider === "moonshot" && this.data[MOONSHOT_OAUTH_KEY]) {
|
|
1182
|
+
return false;
|
|
1183
|
+
}
|
|
1184
|
+
return STATIC_API_KEY_PROVIDERS.has(provider);
|
|
1185
|
+
}
|
|
1186
|
+
async load() {
|
|
1187
|
+
await withFileLock(this.filePath, async () => {
|
|
1188
|
+
try {
|
|
1189
|
+
const content = await fs4.readFile(this.filePath, "utf-8");
|
|
1190
|
+
this.data = JSON.parse(content);
|
|
1191
|
+
log("INFO", "auth", `Loaded credentials from ${this.filePath}`, {
|
|
1192
|
+
providers: Object.keys(this.data).join(",") || "(none)"
|
|
1193
|
+
});
|
|
1194
|
+
} catch (err) {
|
|
1195
|
+
this.data = {};
|
|
1196
|
+
const code = err.code;
|
|
1197
|
+
if (code === "ENOENT") {
|
|
1198
|
+
log("INFO", "auth", `No auth file found at ${this.filePath} (first run)`);
|
|
1199
|
+
} else {
|
|
1200
|
+
log(
|
|
1201
|
+
"ERROR",
|
|
1202
|
+
"auth",
|
|
1203
|
+
`Failed to load auth file: ${err instanceof Error ? err.message : String(err)}`,
|
|
1204
|
+
{ path: this.filePath, code: code ?? "unknown" }
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
this.loaded = true;
|
|
1210
|
+
}
|
|
1211
|
+
async ensureLoaded() {
|
|
1212
|
+
if (!this.loaded) await this.load();
|
|
1213
|
+
}
|
|
1214
|
+
async getCredentials(provider) {
|
|
1215
|
+
await this.ensureLoaded();
|
|
1216
|
+
return this.data[provider];
|
|
1217
|
+
}
|
|
1218
|
+
async setCredentials(provider, creds) {
|
|
1219
|
+
await this.ensureLoaded();
|
|
1220
|
+
this.data[provider] = creds;
|
|
1221
|
+
await this.save();
|
|
1222
|
+
}
|
|
1223
|
+
async clearCredentials(provider) {
|
|
1224
|
+
await this.ensureLoaded();
|
|
1225
|
+
delete this.data[provider];
|
|
1226
|
+
await this.save();
|
|
1227
|
+
}
|
|
1228
|
+
async clearAll() {
|
|
1229
|
+
this.data = {};
|
|
1230
|
+
await this.save();
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Returns valid credentials, auto-refreshing if expired.
|
|
1234
|
+
* If `forceRefresh` is true, refreshes even if the token hasn't expired
|
|
1235
|
+
* (useful when the provider rejects a token with 401 before its stored expiry).
|
|
1236
|
+
* Throws if not logged in.
|
|
1237
|
+
*/
|
|
1238
|
+
async resolveCredentials(provider, opts) {
|
|
1239
|
+
await this.ensureLoaded();
|
|
1240
|
+
if (opts?.storageKeys && !(opts.storageKeys.length === 1 && opts.storageKeys[0] === provider)) {
|
|
1241
|
+
for (const key of opts.storageKeys) {
|
|
1242
|
+
const creds2 = this.data[key];
|
|
1243
|
+
if (creds2) return creds2;
|
|
1244
|
+
}
|
|
1245
|
+
throw new NotLoggedInError(provider);
|
|
1246
|
+
}
|
|
1247
|
+
if (provider === "moonshot" && this.data[MOONSHOT_OAUTH_KEY]) {
|
|
1248
|
+
try {
|
|
1249
|
+
return await this.resolveCredentials(MOONSHOT_OAUTH_KEY, opts);
|
|
1250
|
+
} catch (err) {
|
|
1251
|
+
if (err instanceof NotLoggedInError && this.data["moonshot"]) {
|
|
1252
|
+
log(
|
|
1253
|
+
"WARN",
|
|
1254
|
+
"auth",
|
|
1255
|
+
'Kimi OAuth credential is no longer valid \u2014 falling back to the Moonshot API key. Run "ggcoder login" and choose Kimi OAuth to restore OAuth auth.'
|
|
1256
|
+
);
|
|
1257
|
+
return this.data["moonshot"];
|
|
1258
|
+
}
|
|
1259
|
+
throw err;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
const creds = this.data[provider];
|
|
1263
|
+
if (!creds) {
|
|
1264
|
+
throw new NotLoggedInError(provider);
|
|
1265
|
+
}
|
|
1266
|
+
if (STATIC_API_KEY_PROVIDERS.has(provider)) {
|
|
1267
|
+
return creds;
|
|
1268
|
+
}
|
|
1269
|
+
if (!opts?.forceRefresh && Date.now() < creds.expiresAt - REFRESH_SKEW_MS) {
|
|
1270
|
+
return creds;
|
|
1271
|
+
}
|
|
1272
|
+
const existing = this.refreshLocks.get(provider);
|
|
1273
|
+
if (existing) return existing;
|
|
1274
|
+
const refreshPromise = withFileLock(this.filePath, async () => {
|
|
1275
|
+
try {
|
|
1276
|
+
const content = await fs4.readFile(this.filePath, "utf-8");
|
|
1277
|
+
const freshData = JSON.parse(content);
|
|
1278
|
+
const freshCreds = freshData[provider];
|
|
1279
|
+
if (freshCreds && !opts?.forceRefresh && Date.now() < freshCreds.expiresAt - REFRESH_SKEW_MS) {
|
|
1280
|
+
this.data[provider] = freshCreds;
|
|
1281
|
+
return freshCreds;
|
|
1282
|
+
}
|
|
1283
|
+
} catch {
|
|
1284
|
+
}
|
|
1285
|
+
const refreshFn = provider === "anthropic" ? refreshAnthropicToken : provider === "gemini" ? refreshGeminiToken : provider === MOONSHOT_OAUTH_KEY ? refreshKimiToken : refreshOpenAIToken;
|
|
1286
|
+
let refreshed;
|
|
1287
|
+
try {
|
|
1288
|
+
refreshed = await refreshFn(creds.refreshToken);
|
|
1289
|
+
} catch (err) {
|
|
1290
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1291
|
+
const isAuthFailure = /\((401|400)\)/.test(msg) || /invalid_grant|invalid_token|invalid.*refresh/i.test(msg) || /unauthorized/i.test(msg);
|
|
1292
|
+
if (isAuthFailure) {
|
|
1293
|
+
delete this.data[provider];
|
|
1294
|
+
await atomicWriteFile(this.filePath, JSON.stringify(this.data, null, 2));
|
|
1295
|
+
throw new NotLoggedInError(provider);
|
|
1296
|
+
}
|
|
1297
|
+
throw err;
|
|
1298
|
+
}
|
|
1299
|
+
if (!refreshed.accountId && creds.accountId) {
|
|
1300
|
+
refreshed.accountId = creds.accountId;
|
|
1301
|
+
}
|
|
1302
|
+
if (!refreshed.projectId && creds.projectId) {
|
|
1303
|
+
refreshed.projectId = creds.projectId;
|
|
1304
|
+
}
|
|
1305
|
+
if (!refreshed.baseUrl && creds.baseUrl) {
|
|
1306
|
+
refreshed.baseUrl = creds.baseUrl;
|
|
1307
|
+
}
|
|
1308
|
+
this.data[provider] = refreshed;
|
|
1309
|
+
await atomicWriteFile(this.filePath, JSON.stringify(this.data, null, 2));
|
|
1310
|
+
return refreshed;
|
|
1311
|
+
});
|
|
1312
|
+
this.refreshLocks.set(provider, refreshPromise);
|
|
1313
|
+
try {
|
|
1314
|
+
return await refreshPromise;
|
|
1315
|
+
} finally {
|
|
1316
|
+
this.refreshLocks.delete(provider);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Returns a valid access token, auto-refreshing if expired.
|
|
1321
|
+
* Throws if not logged in.
|
|
1322
|
+
*/
|
|
1323
|
+
async resolveToken(provider) {
|
|
1324
|
+
const creds = await this.resolveCredentials(provider);
|
|
1325
|
+
return creds.accessToken;
|
|
1326
|
+
}
|
|
1327
|
+
async save() {
|
|
1328
|
+
await withFileLock(this.filePath, async () => {
|
|
1329
|
+
await atomicWriteFile(this.filePath, JSON.stringify(this.data, null, 2));
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
async function atomicWriteFile(filePath, content) {
|
|
1334
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.${crypto5.randomUUID().slice(0, 8)}.tmp`;
|
|
1335
|
+
try {
|
|
1336
|
+
await fs4.writeFile(tmpPath, content, { encoding: "utf-8", mode: 384 });
|
|
1337
|
+
await fs4.rename(tmpPath, filePath);
|
|
1338
|
+
} catch (err) {
|
|
1339
|
+
await fs4.unlink(tmpPath).catch(() => {
|
|
1340
|
+
});
|
|
1341
|
+
throw err;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
var NotLoggedInError = class extends Error {
|
|
1345
|
+
provider;
|
|
1346
|
+
constructor(provider) {
|
|
1347
|
+
super(`Not logged in to ${provider}. Run "ggcoder login" to authenticate.`);
|
|
1348
|
+
this.name = "NotLoggedInError";
|
|
1349
|
+
this.provider = provider;
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
// src/model-registry.ts
|
|
1354
|
+
var MODELS = [
|
|
1355
|
+
// ── Anthropic ──────────────────────────────────────────
|
|
1356
|
+
// NOTE: Claude Fable 5 (`claude-fable-5`) and Claude Mythos 5
|
|
1357
|
+
// (`claude-mythos-5`) are temporarily unavailable, so they're commented out
|
|
1358
|
+
// here to keep them out of the /model selector and avoid user confusion.
|
|
1359
|
+
// Re-enable once they're generally available again.
|
|
1360
|
+
// {
|
|
1361
|
+
// id: "claude-fable-5",
|
|
1362
|
+
// name: "Claude Fable 5",
|
|
1363
|
+
// provider: "anthropic",
|
|
1364
|
+
// contextWindow: 1_000_000,
|
|
1365
|
+
// maxOutputTokens: 128_000,
|
|
1366
|
+
// supportsThinking: true,
|
|
1367
|
+
// supportsImages: true,
|
|
1368
|
+
// supportsVideo: false,
|
|
1369
|
+
// costTier: "high",
|
|
1370
|
+
// maxThinkingLevel: "max",
|
|
1371
|
+
// },
|
|
1372
|
+
// {
|
|
1373
|
+
// // Mythos-class model offered through Project Glasswing (limited
|
|
1374
|
+
// // availability, invitation-only). Same underlying model as Fable 5 with
|
|
1375
|
+
// // some safeguards lifted; kept here so approved accounts can select it.
|
|
1376
|
+
// id: "claude-mythos-5",
|
|
1377
|
+
// name: "Claude Mythos 5",
|
|
1378
|
+
// provider: "anthropic",
|
|
1379
|
+
// contextWindow: 1_000_000,
|
|
1380
|
+
// maxOutputTokens: 128_000,
|
|
1381
|
+
// supportsThinking: true,
|
|
1382
|
+
// supportsImages: true,
|
|
1383
|
+
// supportsVideo: false,
|
|
1384
|
+
// costTier: "high",
|
|
1385
|
+
// maxThinkingLevel: "max",
|
|
1386
|
+
// },
|
|
1387
|
+
{
|
|
1388
|
+
id: "claude-opus-4-8",
|
|
1389
|
+
name: "Claude Opus 4.8",
|
|
1390
|
+
provider: "anthropic",
|
|
1391
|
+
contextWindow: 1e6,
|
|
1392
|
+
maxOutputTokens: 128e3,
|
|
1393
|
+
supportsThinking: true,
|
|
1394
|
+
supportsImages: true,
|
|
1395
|
+
supportsVideo: false,
|
|
1396
|
+
costTier: "high",
|
|
1397
|
+
maxThinkingLevel: "max"
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
id: "claude-sonnet-5",
|
|
1401
|
+
name: "Claude Sonnet 5",
|
|
1402
|
+
provider: "anthropic",
|
|
1403
|
+
contextWindow: 1e6,
|
|
1404
|
+
maxOutputTokens: 128e3,
|
|
1405
|
+
supportsThinking: true,
|
|
1406
|
+
supportsImages: true,
|
|
1407
|
+
supportsVideo: false,
|
|
1408
|
+
costTier: "medium",
|
|
1409
|
+
maxThinkingLevel: "max"
|
|
1410
|
+
},
|
|
1411
|
+
{
|
|
1412
|
+
id: "claude-haiku-4-5-20251001",
|
|
1413
|
+
name: "Claude Haiku 4.5",
|
|
1414
|
+
provider: "anthropic",
|
|
1415
|
+
contextWindow: 2e5,
|
|
1416
|
+
maxOutputTokens: 64e3,
|
|
1417
|
+
supportsThinking: true,
|
|
1418
|
+
supportsImages: true,
|
|
1419
|
+
supportsVideo: false,
|
|
1420
|
+
costTier: "low",
|
|
1421
|
+
maxThinkingLevel: "high"
|
|
1422
|
+
},
|
|
1423
|
+
// ── OpenAI (Codex) ─────────────────────────────────────
|
|
1424
|
+
{
|
|
1425
|
+
id: "gpt-5.5",
|
|
1426
|
+
name: "GPT-5.5",
|
|
1427
|
+
provider: "openai",
|
|
1428
|
+
contextWindow: 105e4,
|
|
1429
|
+
codexContextWindow: 272e3,
|
|
1430
|
+
maxOutputTokens: 128e3,
|
|
1431
|
+
supportsThinking: true,
|
|
1432
|
+
supportsImages: true,
|
|
1433
|
+
supportsVideo: false,
|
|
1434
|
+
costTier: "high",
|
|
1435
|
+
maxThinkingLevel: "xhigh"
|
|
1436
|
+
},
|
|
1437
|
+
{
|
|
1438
|
+
id: "gpt-5.4",
|
|
1439
|
+
name: "GPT-5.4",
|
|
1440
|
+
provider: "openai",
|
|
1441
|
+
contextWindow: 105e4,
|
|
1442
|
+
codexContextWindow: 272e3,
|
|
1443
|
+
maxOutputTokens: 128e3,
|
|
1444
|
+
supportsThinking: true,
|
|
1445
|
+
supportsImages: true,
|
|
1446
|
+
supportsVideo: false,
|
|
1447
|
+
costTier: "high",
|
|
1448
|
+
maxThinkingLevel: "xhigh"
|
|
1449
|
+
},
|
|
1450
|
+
{
|
|
1451
|
+
id: "gpt-5.4-mini",
|
|
1452
|
+
name: "GPT-5.4 Mini",
|
|
1453
|
+
provider: "openai",
|
|
1454
|
+
contextWindow: 4e5,
|
|
1455
|
+
maxOutputTokens: 128e3,
|
|
1456
|
+
supportsThinking: true,
|
|
1457
|
+
supportsImages: true,
|
|
1458
|
+
supportsVideo: false,
|
|
1459
|
+
costTier: "low",
|
|
1460
|
+
maxThinkingLevel: "xhigh"
|
|
1461
|
+
},
|
|
1462
|
+
{
|
|
1463
|
+
id: "gpt-5.3-codex",
|
|
1464
|
+
name: "GPT-5.3 Codex",
|
|
1465
|
+
provider: "openai",
|
|
1466
|
+
contextWindow: 4e5,
|
|
1467
|
+
maxOutputTokens: 128e3,
|
|
1468
|
+
supportsThinking: true,
|
|
1469
|
+
supportsImages: true,
|
|
1470
|
+
supportsVideo: false,
|
|
1471
|
+
costTier: "high",
|
|
1472
|
+
maxThinkingLevel: "xhigh"
|
|
1473
|
+
},
|
|
1474
|
+
// ── Sakana (Fugu) ──────────────────────────────────────
|
|
1475
|
+
// Sakana Fugu is a multi-agent system surfaced as a standard LLM via the
|
|
1476
|
+
// OpenAI-compatible Sakana API (https://api.sakana.ai/v1). Both models take
|
|
1477
|
+
// text + image input and only accept "high"/"xhigh" reasoning effort, so the
|
|
1478
|
+
// top tier is `xhigh`. `fugu` routes across all providers; `fugu-ultra` is
|
|
1479
|
+
// the heavier tier (may need larger client timeouts on complex tasks).
|
|
1480
|
+
{
|
|
1481
|
+
id: "fugu",
|
|
1482
|
+
name: "Fugu",
|
|
1483
|
+
provider: "sakana",
|
|
1484
|
+
contextWindow: 1e6,
|
|
1485
|
+
maxOutputTokens: 128e3,
|
|
1486
|
+
supportsThinking: true,
|
|
1487
|
+
supportsImages: true,
|
|
1488
|
+
supportsVideo: false,
|
|
1489
|
+
costTier: "medium",
|
|
1490
|
+
maxThinkingLevel: "xhigh"
|
|
1491
|
+
},
|
|
1492
|
+
{
|
|
1493
|
+
id: "fugu-ultra",
|
|
1494
|
+
name: "Fugu Ultra",
|
|
1495
|
+
provider: "sakana",
|
|
1496
|
+
contextWindow: 1e6,
|
|
1497
|
+
maxOutputTokens: 128e3,
|
|
1498
|
+
supportsThinking: true,
|
|
1499
|
+
supportsImages: true,
|
|
1500
|
+
supportsVideo: false,
|
|
1501
|
+
costTier: "high",
|
|
1502
|
+
maxThinkingLevel: "xhigh"
|
|
1503
|
+
},
|
|
1504
|
+
// ── Gemini ─────────────────────────────────────────────
|
|
1505
|
+
{
|
|
1506
|
+
id: "gemini-3.1-flash-lite-preview",
|
|
1507
|
+
name: "Gemini 3.1 Flash Lite Preview",
|
|
1508
|
+
provider: "gemini",
|
|
1509
|
+
contextWindow: 1048576,
|
|
1510
|
+
maxOutputTokens: 65536,
|
|
1511
|
+
supportsThinking: true,
|
|
1512
|
+
supportsImages: true,
|
|
1513
|
+
supportsVideo: true,
|
|
1514
|
+
maxVideoBytes: 20 * 1024 * 1024,
|
|
1515
|
+
costTier: "low",
|
|
1516
|
+
maxThinkingLevel: "high"
|
|
1517
|
+
},
|
|
1518
|
+
{
|
|
1519
|
+
id: "gemini-3.5-flash",
|
|
1520
|
+
name: "Gemini 3.5 Flash",
|
|
1521
|
+
provider: "gemini",
|
|
1522
|
+
contextWindow: 1048576,
|
|
1523
|
+
maxOutputTokens: 65536,
|
|
1524
|
+
supportsThinking: true,
|
|
1525
|
+
supportsImages: true,
|
|
1526
|
+
supportsVideo: true,
|
|
1527
|
+
maxVideoBytes: 20 * 1024 * 1024,
|
|
1528
|
+
costTier: "low",
|
|
1529
|
+
maxThinkingLevel: "high"
|
|
1530
|
+
},
|
|
1531
|
+
// ── Moonshot (Kimi) ────────────────────────────────────
|
|
1532
|
+
{
|
|
1533
|
+
id: "kimi-k2.7-code",
|
|
1534
|
+
name: "Kimi K2.7",
|
|
1535
|
+
provider: "moonshot",
|
|
1536
|
+
contextWindow: 262144,
|
|
1537
|
+
maxOutputTokens: 262144,
|
|
1538
|
+
supportsThinking: true,
|
|
1539
|
+
supportsImages: true,
|
|
1540
|
+
supportsVideo: true,
|
|
1541
|
+
maxVideoBytes: 100 * 1024 * 1024,
|
|
1542
|
+
costTier: "medium",
|
|
1543
|
+
maxThinkingLevel: "high"
|
|
1544
|
+
},
|
|
1545
|
+
// ── Z.AI (GLM) ─────────────────────────────────────────
|
|
1546
|
+
// GLM-5.2: coding-first flagship with a usable 1M-token context window
|
|
1547
|
+
// (5x jump over GLM-5.1's ~200K) and 131K max output. Released 2026-06-13.
|
|
1548
|
+
{
|
|
1549
|
+
id: "glm-5.2",
|
|
1550
|
+
name: "GLM-5.2",
|
|
1551
|
+
provider: "glm",
|
|
1552
|
+
contextWindow: 1e6,
|
|
1553
|
+
maxOutputTokens: 131072,
|
|
1554
|
+
supportsThinking: true,
|
|
1555
|
+
supportsImages: false,
|
|
1556
|
+
supportsVideo: false,
|
|
1557
|
+
costTier: "medium",
|
|
1558
|
+
maxThinkingLevel: "high"
|
|
1559
|
+
},
|
|
1560
|
+
{
|
|
1561
|
+
id: "glm-5.1",
|
|
1562
|
+
name: "GLM-5.1",
|
|
1563
|
+
provider: "glm",
|
|
1564
|
+
contextWindow: 204800,
|
|
1565
|
+
maxOutputTokens: 131072,
|
|
1566
|
+
supportsThinking: true,
|
|
1567
|
+
supportsImages: false,
|
|
1568
|
+
supportsVideo: false,
|
|
1569
|
+
costTier: "medium",
|
|
1570
|
+
maxThinkingLevel: "high"
|
|
1571
|
+
},
|
|
1572
|
+
{
|
|
1573
|
+
id: "glm-4.7",
|
|
1574
|
+
name: "GLM-4.7",
|
|
1575
|
+
provider: "glm",
|
|
1576
|
+
contextWindow: 2e5,
|
|
1577
|
+
maxOutputTokens: 131072,
|
|
1578
|
+
supportsThinking: true,
|
|
1579
|
+
supportsImages: false,
|
|
1580
|
+
supportsVideo: false,
|
|
1581
|
+
costTier: "low",
|
|
1582
|
+
maxThinkingLevel: "high"
|
|
1583
|
+
},
|
|
1584
|
+
{
|
|
1585
|
+
id: "glm-4.7-flash",
|
|
1586
|
+
name: "GLM-4.7 Flash",
|
|
1587
|
+
provider: "glm",
|
|
1588
|
+
contextWindow: 2e5,
|
|
1589
|
+
maxOutputTokens: 131072,
|
|
1590
|
+
supportsThinking: true,
|
|
1591
|
+
supportsImages: false,
|
|
1592
|
+
supportsVideo: false,
|
|
1593
|
+
costTier: "low",
|
|
1594
|
+
maxThinkingLevel: "high"
|
|
1595
|
+
},
|
|
1596
|
+
// ── MiniMax ────────────────────────────────────────────
|
|
1597
|
+
{
|
|
1598
|
+
id: "MiniMax-M3",
|
|
1599
|
+
name: "MiniMax M3",
|
|
1600
|
+
provider: "minimax",
|
|
1601
|
+
contextWindow: 1e6,
|
|
1602
|
+
maxOutputTokens: 131072,
|
|
1603
|
+
supportsThinking: true,
|
|
1604
|
+
supportsImages: true,
|
|
1605
|
+
supportsVideo: true,
|
|
1606
|
+
maxVideoBytes: 50 * 1024 * 1024,
|
|
1607
|
+
costTier: "medium",
|
|
1608
|
+
maxThinkingLevel: "high"
|
|
1609
|
+
},
|
|
1610
|
+
// ── Xiaomi (MiMo) ──────────────────────────────────────
|
|
1611
|
+
// Pro series: text-only coding/agentic flagship. The legacy mimo-v2-pro
|
|
1612
|
+
// auto-routes to v2.5 on 2026-06-01 and is fully deprecated by 2026-06-30.
|
|
1613
|
+
{
|
|
1614
|
+
id: "mimo-v2.5-pro",
|
|
1615
|
+
name: "MiMo-V2.5-Pro",
|
|
1616
|
+
provider: "xiaomi",
|
|
1617
|
+
contextWindow: 1e6,
|
|
1618
|
+
maxOutputTokens: 131072,
|
|
1619
|
+
supportsThinking: true,
|
|
1620
|
+
supportsImages: false,
|
|
1621
|
+
supportsVideo: false,
|
|
1622
|
+
costTier: "medium",
|
|
1623
|
+
maxThinkingLevel: "high",
|
|
1624
|
+
authStorageKeys: ["xiaomi", XIAOMI_CREDITS_KEY]
|
|
1625
|
+
},
|
|
1626
|
+
// UltraSpeed: lower-latency sibling of the Pro coding flagship, same
|
|
1627
|
+
// text-only capability surface, premium-priced for the throughput gain.
|
|
1628
|
+
// API-only — not served over the Token Plan endpoint, so credentials
|
|
1629
|
+
// resolve from the distinct API Credits key only (see authStorageKeys doc).
|
|
1630
|
+
{
|
|
1631
|
+
id: "mimo-v2.5-pro-ultraspeed",
|
|
1632
|
+
name: "MiMo-V2.5-Pro-UltraSpeed",
|
|
1633
|
+
provider: "xiaomi",
|
|
1634
|
+
contextWindow: 1e6,
|
|
1635
|
+
maxOutputTokens: 131072,
|
|
1636
|
+
supportsThinking: true,
|
|
1637
|
+
supportsImages: false,
|
|
1638
|
+
supportsVideo: false,
|
|
1639
|
+
costTier: "high",
|
|
1640
|
+
maxThinkingLevel: "high",
|
|
1641
|
+
authStorageKeys: [XIAOMI_CREDITS_KEY]
|
|
1642
|
+
},
|
|
1643
|
+
// Omni series: native full-modal understanding (image + audio + video).
|
|
1644
|
+
// Video/image ride the OpenAI-compatible transport as base64 data URLs
|
|
1645
|
+
// (`video_url`/`image_url`), which the shared transform already emits.
|
|
1646
|
+
{
|
|
1647
|
+
id: "mimo-v2.5",
|
|
1648
|
+
name: "MiMo-V2.5",
|
|
1649
|
+
provider: "xiaomi",
|
|
1650
|
+
contextWindow: 1e6,
|
|
1651
|
+
maxOutputTokens: 131072,
|
|
1652
|
+
supportsThinking: true,
|
|
1653
|
+
supportsImages: true,
|
|
1654
|
+
supportsVideo: true,
|
|
1655
|
+
maxVideoBytes: 36 * 1024 * 1024,
|
|
1656
|
+
costTier: "medium",
|
|
1657
|
+
maxThinkingLevel: "high",
|
|
1658
|
+
authStorageKeys: ["xiaomi", XIAOMI_CREDITS_KEY]
|
|
1659
|
+
},
|
|
1660
|
+
// ── DeepSeek ───────────────────────────────────────────
|
|
1661
|
+
{
|
|
1662
|
+
id: "deepseek-v4-pro",
|
|
1663
|
+
name: "DeepSeek V4 Pro",
|
|
1664
|
+
provider: "deepseek",
|
|
1665
|
+
contextWindow: 1048576,
|
|
1666
|
+
maxOutputTokens: 384e3,
|
|
1667
|
+
supportsThinking: true,
|
|
1668
|
+
supportsImages: false,
|
|
1669
|
+
supportsVideo: false,
|
|
1670
|
+
costTier: "high",
|
|
1671
|
+
// DeepSeek V4 maps `xhigh` → its internal `max` tier.
|
|
1672
|
+
maxThinkingLevel: "xhigh"
|
|
1673
|
+
},
|
|
1674
|
+
{
|
|
1675
|
+
id: "deepseek-v4-flash",
|
|
1676
|
+
name: "DeepSeek V4 Flash",
|
|
1677
|
+
provider: "deepseek",
|
|
1678
|
+
contextWindow: 1048576,
|
|
1679
|
+
maxOutputTokens: 384e3,
|
|
1680
|
+
supportsThinking: true,
|
|
1681
|
+
supportsImages: false,
|
|
1682
|
+
supportsVideo: false,
|
|
1683
|
+
costTier: "low",
|
|
1684
|
+
maxThinkingLevel: "xhigh"
|
|
1685
|
+
},
|
|
1686
|
+
// ── OpenRouter ─────────────────────────────────────────
|
|
1687
|
+
{
|
|
1688
|
+
id: "qwen/qwen3.6-plus",
|
|
1689
|
+
name: "Qwen3.6-Plus",
|
|
1690
|
+
provider: "openrouter",
|
|
1691
|
+
contextWindow: 1e6,
|
|
1692
|
+
maxOutputTokens: 65536,
|
|
1693
|
+
supportsThinking: true,
|
|
1694
|
+
supportsImages: false,
|
|
1695
|
+
supportsVideo: false,
|
|
1696
|
+
costTier: "medium",
|
|
1697
|
+
maxThinkingLevel: "high"
|
|
1698
|
+
}
|
|
1699
|
+
];
|
|
1700
|
+
function getModel(id) {
|
|
1701
|
+
return MODELS.find((m) => m.id === id);
|
|
1702
|
+
}
|
|
1703
|
+
function getModelsForProvider(provider) {
|
|
1704
|
+
return MODELS.filter((m) => m.provider === provider);
|
|
1705
|
+
}
|
|
1706
|
+
function getAuthStorageKeys(provider, modelId) {
|
|
1707
|
+
const model = MODELS.find((m) => m.id === modelId && m.provider === provider);
|
|
1708
|
+
return model?.authStorageKeys ?? [provider];
|
|
1709
|
+
}
|
|
1710
|
+
function getAuthStorageKey(provider, modelId) {
|
|
1711
|
+
return getAuthStorageKeys(provider, modelId)[0];
|
|
1712
|
+
}
|
|
1713
|
+
var DEFAULT_MAX_VIDEO_BYTES = 20 * 1024 * 1024;
|
|
1714
|
+
function getVideoByteLimit(modelId) {
|
|
1715
|
+
const model = getModel(modelId);
|
|
1716
|
+
if (!model?.supportsVideo) return void 0;
|
|
1717
|
+
return model.maxVideoBytes ?? DEFAULT_MAX_VIDEO_BYTES;
|
|
1718
|
+
}
|
|
1719
|
+
function getDefaultModel(provider) {
|
|
1720
|
+
if (provider === "xiaomi") return MODELS.find((m) => m.id === "mimo-v2.5-pro");
|
|
1721
|
+
if (provider === "openai") return MODELS.find((m) => m.id === "gpt-5.5");
|
|
1722
|
+
if (provider === "gemini") return MODELS.find((m) => m.id === "gemini-3.1-flash-lite-preview");
|
|
1723
|
+
if (provider === "glm") return MODELS.find((m) => m.id === "glm-5.2");
|
|
1724
|
+
if (provider === "moonshot") return MODELS.find((m) => m.id === "kimi-k2.7-code");
|
|
1725
|
+
if (provider === "minimax") return MODELS.find((m) => m.id === "MiniMax-M3");
|
|
1726
|
+
if (provider === "deepseek") return MODELS.find((m) => m.id === "deepseek-v4-pro");
|
|
1727
|
+
if (provider === "openrouter") return MODELS.find((m) => m.id === "qwen/qwen3.6-plus");
|
|
1728
|
+
if (provider === "sakana") return MODELS.find((m) => m.id === "fugu");
|
|
1729
|
+
return MODELS.find((m) => m.id === "claude-sonnet-5");
|
|
1730
|
+
}
|
|
1731
|
+
function usesOpenAICodexTransport(options) {
|
|
1732
|
+
return options?.provider === "openai" && Boolean(options.accountId);
|
|
1733
|
+
}
|
|
1734
|
+
function getContextWindow(modelId, options) {
|
|
1735
|
+
const model = getModel(modelId);
|
|
1736
|
+
if (!model) return 2e5;
|
|
1737
|
+
if (usesOpenAICodexTransport(options) && model.codexContextWindow) {
|
|
1738
|
+
return model.codexContextWindow;
|
|
1739
|
+
}
|
|
1740
|
+
return model.contextWindow;
|
|
1741
|
+
}
|
|
1742
|
+
function getMaxThinkingLevel(modelId) {
|
|
1743
|
+
return getModel(modelId)?.maxThinkingLevel ?? "high";
|
|
1744
|
+
}
|
|
1745
|
+
function getSummaryModel(provider, currentModelId) {
|
|
1746
|
+
if (provider === "anthropic") {
|
|
1747
|
+
return MODELS.find((m) => m.id === "claude-sonnet-5");
|
|
1748
|
+
}
|
|
1749
|
+
if (provider === "openai" || provider === "glm" || provider === "deepseek") {
|
|
1750
|
+
const low = getModelsForProvider(provider).find((m) => m.costTier === "low");
|
|
1751
|
+
if (low) return low;
|
|
1752
|
+
}
|
|
1753
|
+
return getModel(currentModelId) ?? getDefaultModel(provider);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
export {
|
|
1757
|
+
generatePKCE,
|
|
1758
|
+
openLog,
|
|
1759
|
+
getSessionId,
|
|
1760
|
+
isLoggerOpen,
|
|
1761
|
+
log,
|
|
1762
|
+
registerLogCleanup,
|
|
1763
|
+
closeLogger,
|
|
1764
|
+
getClaudeCodeVersion,
|
|
1765
|
+
getClaudeCliUserAgent,
|
|
1766
|
+
loginAnthropic,
|
|
1767
|
+
refreshAnthropicToken,
|
|
1768
|
+
loginOpenAI,
|
|
1769
|
+
refreshOpenAIToken,
|
|
1770
|
+
loginGemini,
|
|
1771
|
+
refreshGeminiToken,
|
|
1772
|
+
kimiCodeBaseUrl,
|
|
1773
|
+
kimiCodingHeaders,
|
|
1774
|
+
isKimiCodingEndpoint,
|
|
1775
|
+
loginKimi,
|
|
1776
|
+
refreshKimiToken,
|
|
1777
|
+
withFileLock,
|
|
1778
|
+
MOONSHOT_OAUTH_KEY,
|
|
1779
|
+
XIAOMI_CREDITS_KEY,
|
|
1780
|
+
AuthStorage,
|
|
1781
|
+
NotLoggedInError,
|
|
1782
|
+
MODELS,
|
|
1783
|
+
getModel,
|
|
1784
|
+
getModelsForProvider,
|
|
1785
|
+
getAuthStorageKeys,
|
|
1786
|
+
getAuthStorageKey,
|
|
1787
|
+
DEFAULT_MAX_VIDEO_BYTES,
|
|
1788
|
+
getVideoByteLimit,
|
|
1789
|
+
getDefaultModel,
|
|
1790
|
+
usesOpenAICodexTransport,
|
|
1791
|
+
getContextWindow,
|
|
1792
|
+
getMaxThinkingLevel,
|
|
1793
|
+
getSummaryModel
|
|
1794
|
+
};
|
|
1795
|
+
//# sourceMappingURL=chunk-IFKGCWX2.js.map
|