@kenkaiiii/gg-core 5.90.0 → 5.91.1

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/NOTICE ADDED
@@ -0,0 +1,52 @@
1
+ GG Coder
2
+ Copyright (C) 2025-2026 KenKai
3
+
4
+ This program is free software: you can redistribute it and/or modify it under
5
+ the terms of the GNU Affero General Public License as published by the Free
6
+ Software Foundation, version 3 only (AGPL-3.0-only). See LICENSE.
7
+
8
+ This program is distributed in the hope that it will be useful, but WITHOUT
9
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
11
+ details.
12
+
13
+ Releases before 4 November 2026 (up to and including v0.102.0 of the desktop
14
+ app and 5.91.0 of the npm packages) were published under the MIT License, and
15
+ copies already received under those terms keep them.
16
+
17
+ --------------------------------------------------------------------------------
18
+ Contributions under the MIT License
19
+
20
+ The WSL-to-Windows-host audio playback in packages/ggcoder/src/core/radio.ts
21
+ (isWsl and tryPlayOnWindowsHost) is derived from contributions by
22
+ creativeprofit22 (pull requests #15 and #16, May 2026), made under these terms:
23
+
24
+ Copyright (c) 2026 creativeprofit22
25
+
26
+ Permission is hereby granted, free of charge, to any person obtaining a copy
27
+ of this software and associated documentation files (the "Software"), to deal
28
+ in the Software without restriction, including without limitation the rights
29
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
30
+ copies of the Software, and to permit persons to whom the Software is
31
+ furnished to do so, subject to the following conditions:
32
+
33
+ The above copyright notice and this permission notice shall be included in
34
+ all copies or substantial portions of the Software.
35
+
36
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
37
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
38
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
39
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
40
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
41
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
42
+ SOFTWARE.
43
+
44
+ --------------------------------------------------------------------------------
45
+ Third-party components
46
+
47
+ Bundled third-party code, fonts and media keep their own licenses, listed in:
48
+ gg-app/src/assets/fonts/THIRD-PARTY.md
49
+ packages/ggcoder/assets/motion/THIRD-PARTY.md
50
+ packages/gg-world/CREDITS.md
51
+ and in the LICENSE / OFL.txt files beside each component. npm and Cargo
52
+ dependencies are installed under their own licenses.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  <p align="center">
8
8
  <a href="https://www.npmjs.com/package/@kenkaiiii/gg-core"><img src="https://img.shields.io/npm/v/@kenkaiiii/gg-core?style=for-the-badge" alt="npm version"></a>
9
- <a href="../../LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge" alt="MIT License"></a>
9
+ <a href="../../LICENSE"><img src="https://img.shields.io/badge/License-AGPL--3.0-blue.svg?style=for-the-badge" alt="AGPL-3.0 License"></a>
10
10
  </p>
11
11
 
12
12
  The shared foundation under GG Coder's app and CLI. It depends only on [`@kenkaiiii/gg-ai`](../gg-ai/README.md), never on React, Ink or the agent loop.
@@ -25,15 +25,15 @@ npm i @kenkaiiii/gg-core
25
25
 
26
26
  ## What's inside
27
27
 
28
- | Area | Main exports |
29
- |---|---|
30
- | Model registry | `getModel`, `getAllModels`, `getModelsForProvider`, `getDefaultModel`, `getContextWindow` |
31
- | Thinking levels | `getSupportedThinkingLevels`, `getMaxThinkingLevel`, `getDefaultThinkingLevel` |
32
- | Auth storage | `AuthStorage` (reads and writes `~/.gg/auth.json`, refreshes OAuth tokens) |
33
- | OAuth | Anthropic, OpenAI, Gemini, Kimi and xAI login flows |
34
- | Local models | `discoverLocalModels` (Ollama, LM Studio, llama.cpp, vLLM, custom endpoints) |
35
- | Paths | `getAppPaths` (every file under `~/.gg/`) |
36
- | Other | Logger, file lock, usage limits, Telegram, voice transcription, auto-update |
28
+ | Area | Main exports |
29
+ | --------------- | ----------------------------------------------------------------------------------------- |
30
+ | Model registry | `getModel`, `getAllModels`, `getModelsForProvider`, `getDefaultModel`, `getContextWindow` |
31
+ | Thinking levels | `getSupportedThinkingLevels`, `getMaxThinkingLevel`, `getDefaultThinkingLevel` |
32
+ | Auth storage | `AuthStorage` (reads and writes `~/.gg/auth.json`, refreshes OAuth tokens) |
33
+ | OAuth | Anthropic, OpenAI, Gemini, Kimi and xAI login flows |
34
+ | Local models | `discoverLocalModels` (Ollama, LM Studio, llama.cpp, vLLM, custom endpoints) |
35
+ | Paths | `getAppPaths` (every file under `~/.gg/`) |
36
+ | Other | Logger, file lock, usage limits, Telegram, voice transcription, auto-update |
37
37
 
38
38
  Two lighter subpath entries skip the rest of the package:
39
39
 
@@ -65,4 +65,4 @@ console.log(models.map((m) => m.id));
65
65
 
66
66
  ## License
67
67
 
68
- MIT
68
+ AGPL-3.0-only. See [LICENSE](../../LICENSE).
@@ -1177,9 +1177,13 @@ async function acquireLock(lockPath) {
1177
1177
  continue;
1178
1178
  }
1179
1179
  } catch {
1180
- await fs.unlink(lockPath).catch(() => {
1181
- });
1182
- continue;
1180
+ const stat = await fs.stat(lockPath).catch(() => null);
1181
+ if (!stat) continue;
1182
+ if (Date.now() - stat.mtimeMs > STALE_TIMEOUT_MS) {
1183
+ await fs.unlink(lockPath).catch(() => {
1184
+ });
1185
+ continue;
1186
+ }
1183
1187
  }
1184
1188
  if (Date.now() - startTime > MAX_WAIT_MS) {
1185
1189
  await fs.unlink(lockPath).catch(() => {
@@ -1376,6 +1380,90 @@ function isHiddenProvider(provider) {
1376
1380
 
1377
1381
  // src/auth-storage.ts
1378
1382
  import { LOCAL_NO_API_KEY } from "@kenkaiiii/gg-ai";
1383
+
1384
+ // src/minimax-region.ts
1385
+ var MINIMAX_GLOBAL_BASE_URL = "https://api.minimax.io/anthropic";
1386
+ var MINIMAX_CN_BASE_URL = "https://api.minimax.cn/anthropic";
1387
+ var REGION_HOSTS = [
1388
+ { host: "https://api.minimax.io", baseUrl: MINIMAX_GLOBAL_BASE_URL },
1389
+ { host: "https://api.minimax.cn", baseUrl: MINIMAX_CN_BASE_URL }
1390
+ ];
1391
+ var KEY_KNOWN_CODES = /* @__PURE__ */ new Set([0, 2062]);
1392
+ var KEY_REJECTED_CODES = /* @__PURE__ */ new Set([1004, 2049]);
1393
+ async function probeRegion(host, apiKey, fetchImpl, timeoutMs) {
1394
+ const url = apiKey.startsWith("sk-api-") ? `${host}/account/query_balance` : `${host}/v1/token_plan/remains`;
1395
+ let sawRejection = false;
1396
+ const authHeaders = [
1397
+ { Authorization: `Bearer ${apiKey}` },
1398
+ { "x-api-key": apiKey }
1399
+ ];
1400
+ for (const auth of authHeaders) {
1401
+ let res;
1402
+ try {
1403
+ res = await fetchImpl(url, {
1404
+ headers: { ...auth, "Content-Type": "application/json" },
1405
+ signal: AbortSignal.timeout(timeoutMs)
1406
+ });
1407
+ } catch {
1408
+ return "inconclusive";
1409
+ }
1410
+ if (res.status === 401 || res.status === 403) {
1411
+ sawRejection = true;
1412
+ continue;
1413
+ }
1414
+ if (!res.ok) return "inconclusive";
1415
+ let code;
1416
+ try {
1417
+ code = (await res.json()).base_resp?.status_code;
1418
+ } catch {
1419
+ return "inconclusive";
1420
+ }
1421
+ if (typeof code === "number" && KEY_KNOWN_CODES.has(code)) return "authorized";
1422
+ if (typeof code === "number" && KEY_REJECTED_CODES.has(code)) {
1423
+ sawRejection = true;
1424
+ continue;
1425
+ }
1426
+ return "inconclusive";
1427
+ }
1428
+ return sawRejection ? "unauthorized" : "inconclusive";
1429
+ }
1430
+ async function detectMiniMaxBaseUrl(apiKey, options = {}) {
1431
+ const fetchImpl = options.fetch ?? fetch;
1432
+ const timeoutMs = options.timeoutMs ?? 5e3;
1433
+ const results = await Promise.all(
1434
+ REGION_HOSTS.map((r) => probeRegion(r.host, apiKey, fetchImpl, timeoutMs))
1435
+ );
1436
+ const index = results.indexOf("authorized");
1437
+ return index === -1 ? void 0 : REGION_HOSTS[index].baseUrl;
1438
+ }
1439
+
1440
+ // src/xiaomi-region.ts
1441
+ var XIAOMI_TOKEN_PLAN_BASE_URLS = [
1442
+ "https://token-plan-sgp.xiaomimimo.com/v1",
1443
+ "https://token-plan-ams.xiaomimimo.com/v1",
1444
+ "https://token-plan-cn.xiaomimimo.com/v1"
1445
+ ];
1446
+ async function detectXiaomiTokenPlanBaseUrl(apiKey, options = {}) {
1447
+ const fetchImpl = options.fetch ?? fetch;
1448
+ const timeoutMs = options.timeoutMs ?? 5e3;
1449
+ const accepted = await Promise.all(
1450
+ XIAOMI_TOKEN_PLAN_BASE_URLS.map(async (baseUrl) => {
1451
+ try {
1452
+ const res = await fetchImpl(`${baseUrl}/models`, {
1453
+ headers: { Authorization: `Bearer ${apiKey}` },
1454
+ signal: AbortSignal.timeout(timeoutMs)
1455
+ });
1456
+ return res.ok;
1457
+ } catch {
1458
+ return false;
1459
+ }
1460
+ })
1461
+ );
1462
+ const index = accepted.indexOf(true);
1463
+ return index === -1 ? void 0 : XIAOMI_TOKEN_PLAN_BASE_URLS[index];
1464
+ }
1465
+
1466
+ // src/auth-storage.ts
1379
1467
  var MOONSHOT_OAUTH_KEY = "moonshot-oauth";
1380
1468
  var XAI_OAUTH_KEY = "xai-oauth";
1381
1469
  var DUAL_AUTH_PROVIDERS = [
@@ -1425,6 +1513,15 @@ function activeBaseUrlEntry(data, provider) {
1425
1513
  }
1426
1514
  return data[provider];
1427
1515
  }
1516
+ var MINIMAX_REGION_RETRY_MS = 5 * 60 * 1e3;
1517
+ var miniMaxRegionProbes = /* @__PURE__ */ new Map();
1518
+ function cachedMiniMaxRegion(apiKey) {
1519
+ const cached = miniMaxRegionProbes.get(apiKey);
1520
+ if (cached && Date.now() - cached.at < MINIMAX_REGION_RETRY_MS) return cached.result;
1521
+ const result = detectMiniMaxBaseUrl(apiKey).catch(() => void 0);
1522
+ miniMaxRegionProbes.set(apiKey, { at: Date.now(), result });
1523
+ return result;
1524
+ }
1428
1525
  function readStoredBaseUrlSync(authFile, provider) {
1429
1526
  try {
1430
1527
  const data = JSON.parse(readFileSync2(authFile, "utf-8"));
@@ -1471,8 +1568,12 @@ var AuthStorage = class {
1471
1568
  snapshotIno = 0;
1472
1569
  /** Per-provider lock to serialize concurrent refresh calls. */
1473
1570
  refreshLocks = /* @__PURE__ */ new Map();
1474
- constructor(filePath) {
1571
+ detectMiniMaxRegion;
1572
+ detectXiaomiCluster;
1573
+ constructor(filePath, options) {
1475
1574
  this.filePath = filePath ?? getAppPaths().authFile;
1575
+ this.detectMiniMaxRegion = options?.detectMiniMaxBaseUrl ?? cachedMiniMaxRegion;
1576
+ this.detectXiaomiCluster = options?.detectXiaomiTokenPlanBaseUrl ?? detectXiaomiTokenPlanBaseUrl;
1476
1577
  }
1477
1578
  /** Path to the on-disk auth file. Useful for status output. */
1478
1579
  get path() {
@@ -1671,11 +1772,48 @@ var AuthStorage = class {
1671
1772
  await this.ensureFresh();
1672
1773
  return this.data[provider];
1673
1774
  }
1775
+ /**
1776
+ * A MiniMax key saved without an endpoint (every key, until its first use)
1777
+ * gets the endpoint of the region that issued it, saved so the check runs
1778
+ * once. Without this a Mainland China key went to the Global API, got a 401,
1779
+ * and was deleted as invalid.
1780
+ */
1781
+ async withMiniMaxRegion(creds) {
1782
+ const baseUrl = await this.detectMiniMaxRegion(creds.accessToken);
1783
+ if (!baseUrl) return creds;
1784
+ log("INFO", "auth", `MiniMax key belongs to ${baseUrl} \u2014 saved for future requests`);
1785
+ await this.mutateLatest((latest) => {
1786
+ const current = latest["minimax"];
1787
+ if (current?.accessToken === creds.accessToken && !current.baseUrl) {
1788
+ current.baseUrl = baseUrl;
1789
+ }
1790
+ });
1791
+ return { ...creds, baseUrl };
1792
+ }
1674
1793
  async setCredentials(provider, creds) {
1675
1794
  await this.mutateLatest((latest) => {
1676
1795
  latest[provider] = creds;
1677
1796
  });
1678
1797
  }
1798
+ /**
1799
+ * After a 401, move a regional key to the cluster that issued it instead of
1800
+ * treating it as invalid. Only the Xiaomi Token Plan is regional: every key
1801
+ * used to go to Singapore, so Europe and China keys were rejected and deleted.
1802
+ * Returns the new base URL, or undefined when no other cluster accepts the key.
1803
+ */
1804
+ async relocateRegionalApiKey(storageKeys) {
1805
+ const key = await this.pickStorageKey(storageKeys);
1806
+ const creds = key === "xiaomi" ? this.data[key] : void 0;
1807
+ if (!creds) return void 0;
1808
+ const baseUrl = await this.detectXiaomiCluster(creds.accessToken);
1809
+ if (!baseUrl || baseUrl === creds.baseUrl) return void 0;
1810
+ await this.mutateLatest((latest) => {
1811
+ const current = latest["xiaomi"];
1812
+ if (current?.accessToken === creds.accessToken) current.baseUrl = baseUrl;
1813
+ });
1814
+ log("INFO", "auth", `Xiaomi Token Plan key belongs to ${baseUrl} \u2014 saved for future requests`);
1815
+ return baseUrl;
1816
+ }
1679
1817
  // Keeps local-endpoint keys: they're the user's own config, and a 401 there is often a busy gateway.
1680
1818
  async clearRejectedApiKey(storageKey) {
1681
1819
  if (storageKey.startsWith(LOCAL_AUTH_KEY_PREFIX)) return false;
@@ -1795,6 +1933,7 @@ var AuthStorage = class {
1795
1933
  throw new NotLoggedInError(provider);
1796
1934
  }
1797
1935
  if (STATIC_API_KEY_PROVIDERS.has(provider)) {
1936
+ if (provider === "minimax" && !creds.baseUrl) return await this.withMiniMaxRegion(creds);
1798
1937
  return creds;
1799
1938
  }
1800
1939
  if (!opts?.forceRefresh && Date.now() < creds.expiresAt - refreshThresholdMs(creds)) {
@@ -2445,6 +2584,23 @@ var MODELS = [
2445
2584
  maxThinkingLevel: "max"
2446
2585
  },
2447
2586
  // ── MiniMax ────────────────────────────────────────────
2587
+ {
2588
+ // The only text model M Plan / Token Plan keys cover; MiniMax-M3 on those
2589
+ // keys fails with "insufficient balance (1008)". Pay-as-you-go serves both.
2590
+ // Always thinks (`thinking: disabled` 400s; omitted = on) with a real
2591
+ // low→max effort ladder on output_config.effort, API default max.
2592
+ id: "MiniMax-M3.1-Flash",
2593
+ name: "MiniMax M3.1 Flash",
2594
+ provider: "minimax",
2595
+ contextWindow: 1e6,
2596
+ maxOutputTokens: 131072,
2597
+ supportsThinking: true,
2598
+ supportsImages: true,
2599
+ supportsVideo: true,
2600
+ maxVideoBytes: 50 * 1024 * 1024,
2601
+ costTier: "low",
2602
+ maxThinkingLevel: "max"
2603
+ },
2448
2604
  {
2449
2605
  id: "MiniMax-M3",
2450
2606
  name: "MiniMax M3",
@@ -2651,11 +2807,44 @@ function clearRuntimeModels(predicate) {
2651
2807
  if (predicate(model)) runtimeModels.delete(id);
2652
2808
  }
2653
2809
  }
2810
+ var providerCatalogs = /* @__PURE__ */ new Map();
2811
+ function setProviderCatalog(provider, models) {
2812
+ if (!models) {
2813
+ providerCatalogs.delete(provider);
2814
+ return [];
2815
+ }
2816
+ const installed = /* @__PURE__ */ new Map();
2817
+ for (const model of models) {
2818
+ if (model.provider !== provider) continue;
2819
+ if (MODELS.some((m) => m.id === model.id && m.provider !== provider)) continue;
2820
+ installed.set(model.id, model);
2821
+ }
2822
+ providerCatalogs.set(provider, installed);
2823
+ return [...installed.values()];
2824
+ }
2825
+ function getProviderCatalog(provider) {
2826
+ const catalog = providerCatalogs.get(provider);
2827
+ return catalog ? [...catalog.values()] : void 0;
2828
+ }
2654
2829
  function getAllModels() {
2655
- return [...MODELS, ...runtimeModels.values()];
2830
+ const synced = [...providerCatalogs.values()].flatMap((c) => [...c.values()]);
2831
+ return [
2832
+ ...MODELS.filter((m) => !providerCatalogs.has(m.provider)),
2833
+ ...synced,
2834
+ ...runtimeModels.values()
2835
+ ];
2656
2836
  }
2657
2837
  function getModel(id) {
2658
- return MODELS.find((m) => m.id === id) ?? runtimeModels.get(id);
2838
+ const fixed = MODELS.find((m) => m.id === id);
2839
+ if (fixed && !providerCatalogs.has(fixed.provider)) return fixed;
2840
+ return findSynced(id) ?? fixed ?? runtimeModels.get(id);
2841
+ }
2842
+ function findSynced(id) {
2843
+ for (const catalog of providerCatalogs.values()) {
2844
+ const hit = catalog.get(id);
2845
+ if (hit) return hit;
2846
+ }
2847
+ return void 0;
2659
2848
  }
2660
2849
  function getModelsForProvider(provider) {
2661
2850
  return getAllModels().filter((m) => m.provider === provider);
@@ -2695,7 +2884,7 @@ var DEFAULT_MODEL_IDS = {
2695
2884
  gemini: "gemini-3.1-flash-lite",
2696
2885
  glm: "glm-5.3",
2697
2886
  moonshot: "kimi-k3",
2698
- minimax: "MiniMax-M3",
2887
+ minimax: "MiniMax-M3.1-Flash",
2699
2888
  deepseek: "deepseek-v4-pro",
2700
2889
  huggingface: "moonshotai/Kimi-K2.7-Code",
2701
2890
  openrouter: "qwen/qwen3.8-max",
@@ -2791,6 +2980,11 @@ export {
2791
2980
  closeLogger,
2792
2981
  HIDDEN_PROVIDERS,
2793
2982
  isHiddenProvider,
2983
+ MINIMAX_GLOBAL_BASE_URL,
2984
+ MINIMAX_CN_BASE_URL,
2985
+ detectMiniMaxBaseUrl,
2986
+ XIAOMI_TOKEN_PLAN_BASE_URLS,
2987
+ detectXiaomiTokenPlanBaseUrl,
2794
2988
  MOONSHOT_OAUTH_KEY,
2795
2989
  XAI_OAUTH_KEY,
2796
2990
  dualAuthProvider,
@@ -2807,6 +3001,8 @@ export {
2807
3001
  MODELS,
2808
3002
  registerRuntimeModels,
2809
3003
  clearRuntimeModels,
3004
+ setProviderCatalog,
3005
+ getProviderCatalog,
2810
3006
  getAllModels,
2811
3007
  getModel,
2812
3008
  getModelsForProvider,
@@ -2825,4 +3021,4 @@ export {
2825
3021
  getDefaultThinkingLevel,
2826
3022
  getSummaryModel
2827
3023
  };
2828
- //# sourceMappingURL=chunk-QL5357JB.js.map
3024
+ //# sourceMappingURL=chunk-YXPGQIOD.js.map