@neuralnomads/codenomad-dev 0.19.0-dev-20260827-14586cb3 → 0.19.0-dev-20260829-56156827

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.
@@ -1,6 +1,10 @@
1
1
  import { fetch } from "undici";
2
2
  import { compareVersionStrings, stripTagPrefix } from "./release-monitor";
3
3
  const DEFAULT_POLL_INTERVAL_MS = 15 * 60 * 1000;
4
+ export function matchesDevReleaseChannel(tag, currentVersion) {
5
+ const v2 = "-dev-v2-";
6
+ return currentVersion.includes(v2) ? tag.includes(v2) : tag.includes("-dev-") && !tag.includes(v2);
7
+ }
4
8
  export function startDevReleaseMonitor(options) {
5
9
  let stopped = false;
6
10
  let timer = null;
@@ -49,7 +53,10 @@ async function fetchLatestPrerelease(args) {
49
53
  throw new Error(`GitHub releases API responded with ${response.status}`);
50
54
  }
51
55
  const list = (await response.json());
52
- const latest = list.find((r) => r && r.prerelease === true && r.draft !== true);
56
+ const latest = list.find((release) => {
57
+ const tag = release?.tag_name || release?.name || "";
58
+ return release?.prerelease === true && release.draft !== true && matchesDevReleaseChannel(tag, args.currentVersion);
59
+ });
53
60
  if (!latest) {
54
61
  return null;
55
62
  }
@@ -0,0 +1,11 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { matchesDevReleaseChannel } from "./dev-release-monitor";
4
+ test("dev prereleases stay within their V1 or V2 channel", () => {
5
+ const v1 = "v0.19.0-dev-20260825-12345678";
6
+ const v2 = "v0.20.0-dev-v2-20260825-abcdef12";
7
+ assert.equal(matchesDevReleaseChannel(v1, v1), true);
8
+ assert.equal(matchesDevReleaseChannel(v2, v1), false);
9
+ assert.equal(matchesDevReleaseChannel(v2, v2), true);
10
+ assert.equal(matchesDevReleaseChannel(v1, v2), false);
11
+ });
@@ -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-20260827-14586cb3",
3
+ "version": "0.19.0-dev-20260829-56156827",
4
4
  "description": "CodeNomad Server",
5
5
  "license": "MIT",
6
6
  "author": {