@neuralnomads/codenomad-dev 0.19.0-dev-20260828-0fbf7b25 → 0.19.0-dev-20260830-e4cd6d6f

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.
@@ -96,6 +96,12 @@ const openRouter = {
96
96
  });
97
97
  },
98
98
  };
99
+ const CREDIT_LIMIT_TYPE = "CREDIT_LIMIT";
100
+ const UNIT_SECONDS = { 3: 3_600, 6: 604_800 };
101
+ function unitWindowSeconds(unit, count) {
102
+ const unitSeconds = typeof unit === "number" ? UNIT_SECONDS[unit] : undefined;
103
+ return unitSeconds !== undefined && count !== null ? unitSeconds * count : null;
104
+ }
99
105
  function createTokenLimitProvider(input) {
100
106
  return {
101
107
  id: input.id,
@@ -109,16 +115,29 @@ function createTokenLimitProvider(input) {
109
115
  const payload = await fetchJson(input.url, { headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" } });
110
116
  const windows = {};
111
117
  for (const limit of Array.isArray(payload?.data?.limits) ? payload.data.limits : []) {
112
- if (limit?.type !== "TOKENS_LIMIT" && limit?.type !== "TIME_LIMIT")
113
- continue;
114
- const duration = toNumber(limit.number);
115
- const seconds = limit.type === "TIME_LIMIT" ? 30 * 86_400 : limit.unit === 3 && duration ? duration * 3600 : null;
116
- const label = limit.type === "TIME_LIMIT" ? "mcp-tools" : resolveWindowLabel(seconds);
117
- windows[label] = toUsageWindow({
118
- usedPercent: toNumber(limit.percentage),
119
- windowSeconds: seconds,
120
- resetAt: toTimestamp(limit.nextResetTime),
121
- });
118
+ const number = toNumber(limit?.number);
119
+ if (limit && (limit.type === "TOKENS_LIMIT" || limit.type === "TIME_LIMIT")) {
120
+ const seconds = limit.type === "TIME_LIMIT" ? 30 * 86_400 : unitWindowSeconds(limit.unit, number);
121
+ const label = limit.type === "TIME_LIMIT" ? "mcp-tools" : resolveWindowLabel(seconds);
122
+ windows[label] = toUsageWindow({
123
+ usedPercent: toNumber(limit.percentage),
124
+ windowSeconds: seconds,
125
+ resetAt: toTimestamp(limit.nextResetTime),
126
+ });
127
+ }
128
+ else if (limit && limit.type === CREDIT_LIMIT_TYPE) {
129
+ const usedPercent = toNumber(limit.percentage);
130
+ const remaining = toNumber(limit.remaining);
131
+ const allowance = toNumber(limit.usage);
132
+ if (usedPercent === null && (remaining === null || allowance === null))
133
+ continue;
134
+ const seconds = unitWindowSeconds(limit.unit, number);
135
+ windows[resolveWindowLabel(seconds)] = toUsageWindow({
136
+ usedPercent: usedPercent ?? (allowance !== null && allowance > 0 && remaining !== null ? ((allowance - remaining) / allowance) * 100 : null),
137
+ windowSeconds: seconds,
138
+ resetAt: toTimestamp(limit.nextResetTime),
139
+ });
140
+ }
122
141
  }
123
142
  return { windows };
124
143
  });
@@ -0,0 +1,110 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import { apiKeyProviders } from "./api-key";
7
+ const originalAuthFile = process.env.OPENCODE_AUTH_FILE;
8
+ const createdAuthDirs = [];
9
+ test.before(() => {
10
+ const authDir = fs.mkdtempSync(path.join(os.tmpdir(), "codenomad-usage-auth-"));
11
+ createdAuthDirs.push(authDir);
12
+ fs.writeFileSync(path.join(authDir, "auth.json"), JSON.stringify({ "zai-coding-plan": { type: "api", key: "test-zai-key" }, "zhipuai-coding-plan": { type: "api", key: "test-zhipu-key" } }));
13
+ process.env.OPENCODE_AUTH_FILE = path.join(authDir, "auth.json");
14
+ });
15
+ test.after(() => {
16
+ if (originalAuthFile === undefined)
17
+ delete process.env.OPENCODE_AUTH_FILE;
18
+ else
19
+ process.env.OPENCODE_AUTH_FILE = originalAuthFile;
20
+ const tmpRoot = fs.realpathSync(os.tmpdir());
21
+ for (const dir of createdAuthDirs) {
22
+ // Containment guard: never recurse outside the temp root we created.
23
+ if (fs.realpathSync(path.dirname(dir)) === tmpRoot && path.basename(dir).startsWith("codenomad-usage-auth-")) {
24
+ fs.rmSync(dir, { recursive: true, force: true });
25
+ }
26
+ }
27
+ });
28
+ const CREDIT_RESET_AT = 1787849891189;
29
+ const TOKENS_RESET_AT = 1787950772998;
30
+ function respondWith(payload) {
31
+ return new Response(JSON.stringify(payload), { status: 200, headers: { "Content-Type": "application/json" } });
32
+ }
33
+ async function withFetchStub(payload, run) {
34
+ const original = globalThis.fetch;
35
+ let requestUrl;
36
+ globalThis.fetch = (async (input) => {
37
+ requestUrl = String(input);
38
+ return respondWith(payload);
39
+ });
40
+ try {
41
+ return { result: await run(), requestUrl };
42
+ }
43
+ finally {
44
+ globalThis.fetch = original;
45
+ }
46
+ }
47
+ const zai = apiKeyProviders.find((provider) => provider.id === "zai-coding-plan");
48
+ const zhipu = apiKeyProviders.find((provider) => provider.id === "zhipuai-coding-plan");
49
+ test("parses credit-based limits into usage windows", async () => {
50
+ const { result } = await withFetchStub({
51
+ code: 200,
52
+ msg: "Operation successful",
53
+ success: true,
54
+ data: {
55
+ level: "lite",
56
+ limits: [
57
+ { type: "CREDIT_LIMIT", unit: 3, number: 5, usage: 2000, currentValue: 157, remaining: 1842, percentage: 7, nextResetTime: CREDIT_RESET_AT },
58
+ { type: "CREDIT_LIMIT", unit: 6, number: 1, usage: 10000, currentValue: 157, remaining: 9843, percentage: 1, nextResetTime: TOKENS_RESET_AT },
59
+ ],
60
+ },
61
+ }, () => zai.fetchQuota());
62
+ assert.equal(result.ok, true);
63
+ const windows = result.usage.windows;
64
+ assert.equal(windows["5h"]?.usedPercent, 7);
65
+ assert.equal(windows["5h"]?.windowSeconds, 18_000);
66
+ assert.equal(windows["5h"]?.resetAt, CREDIT_RESET_AT);
67
+ assert.equal(windows["weekly"]?.usedPercent, 1);
68
+ assert.equal(windows["weekly"]?.windowSeconds, 604_800);
69
+ assert.equal("valueLabel" in (windows["weekly"] ?? {}), false);
70
+ });
71
+ test("derives usedPercent when the credit payload omits percentage", async () => {
72
+ const { result } = await withFetchStub({
73
+ data: { limits: [{ type: "CREDIT_LIMIT", unit: 6, number: 1, usage: 2000, currentValue: 158, remaining: 1842, nextResetTime: CREDIT_RESET_AT }] },
74
+ }, () => zai.fetchQuota());
75
+ const window = result.usage.windows["weekly"];
76
+ // (2000 - 1842) / 2000
77
+ assert.ok(Math.abs((window?.usedPercent ?? 0) - 7.9) < 0.001);
78
+ assert.equal(window?.windowSeconds, 604_800);
79
+ });
80
+ test("coerces string quota fields and skips unusable or unknown entries without throwing", async () => {
81
+ const { result } = await withFetchStub({
82
+ data: {
83
+ limits: [
84
+ null,
85
+ { type: "UNKNOWN_LIMIT" },
86
+ { type: "PROMO_LIMIT", percentage: 55, remaining: 10, usage: 20 },
87
+ { type: "CREDIT_LIMIT", unit: 6, number: 1, usage: "10000", remaining: "9800", percentage: "2", nextResetTime: TOKENS_RESET_AT },
88
+ ],
89
+ },
90
+ }, () => zai.fetchQuota());
91
+ assert.equal(result.ok, true);
92
+ const windows = result.usage.windows;
93
+ assert.equal(Object.keys(windows).length, 1);
94
+ assert.equal(windows["weekly"]?.usedPercent, 2);
95
+ });
96
+ test("keeps legacy token and time windows intact for the bigmodel endpoint", async () => {
97
+ const { result, requestUrl } = await withFetchStub({
98
+ data: {
99
+ limits: [
100
+ { type: "TOKENS_LIMIT", unit: 3, number: 1, percentage: 42, nextResetTime: TOKENS_RESET_AT },
101
+ { type: "TIME_LIMIT", number: 1, percentage: 11 },
102
+ ],
103
+ },
104
+ }, () => zhipu.fetchQuota());
105
+ assert.equal(requestUrl, "https://open.bigmodel.cn/api/monitor/usage/quota/limit");
106
+ assert.equal(result.ok, true);
107
+ const windows = result.usage.windows;
108
+ assert.equal(windows["1h"]?.usedPercent, 42);
109
+ assert.equal(windows["mcp-tools"]?.usedPercent, 11);
110
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neuralnomads/codenomad-dev",
3
- "version": "0.19.0-dev-20260828-0fbf7b25",
3
+ "version": "0.19.0-dev-20260830-e4cd6d6f",
4
4
  "description": "CodeNomad Server",
5
5
  "license": "MIT",
6
6
  "author": {
package/public/index.html CHANGED
@@ -30,8 +30,8 @@
30
30
  <link rel="modulepreload" crossorigin href="/assets/git-diff-vendor-CYS3ApzA.js">
31
31
  <link rel="modulepreload" crossorigin href="/assets/monaco-viewer-NKl8kwH2.js">
32
32
  <link rel="modulepreload" crossorigin href="/assets/index-DzWXCPie.js">
33
- <link rel="stylesheet" crossorigin href="/assets/index-By9pgzQk.css">
34
33
  <link rel="stylesheet" crossorigin href="/assets/git-diff-vendor-HAZkIolJ.css">
34
+ <link rel="stylesheet" crossorigin href="/assets/index-By9pgzQk.css">
35
35
  <link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script>
36
36
  <meta name="theme-color" content="#1a1a1a">
37
37
  <link rel="icon" href="/favicon.ico" sizes="48x48">
@@ -20,8 +20,8 @@
20
20
  <link rel="modulepreload" crossorigin href="/assets/git-diff-vendor-CYS3ApzA.js">
21
21
  <link rel="modulepreload" crossorigin href="/assets/monaco-viewer-NKl8kwH2.js">
22
22
  <link rel="modulepreload" crossorigin href="/assets/index-DzWXCPie.js">
23
- <link rel="stylesheet" crossorigin href="/assets/git-diff-vendor-HAZkIolJ.css">
24
23
  <link rel="stylesheet" crossorigin href="/assets/index-By9pgzQk.css">
24
+ <link rel="stylesheet" crossorigin href="/assets/git-diff-vendor-HAZkIolJ.css">
25
25
  <link rel="stylesheet" crossorigin href="/assets/loading-CmEVQgyj.css">
26
26
  <link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script>
27
27
  <meta name="theme-color" content="#1a1a1a">