Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
delete ctx.__codexbarNowMillis;
ctx.date = Object.freeze({
now() { return parseDate(nowMillis); },
nowMillis() { return nowMillis; },
iso(value) { return parseDate(String(value)); },
unixSeconds(value) { return parseDate(Number(value) * 1000); },
unixMillis(value) { return parseDate(Number(value)); },
Expand Down
44 changes: 44 additions & 0 deletions Sources/CodexBarCore/Resources/Plugins/zai.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,48 @@ defineProvider({
if (limit.remaining !== null) parts.push(`${limit.remaining} remaining`);
return { label, value: `${limit.percent.toFixed(limit.percent % 1 ? 1 : 0)}% used`, secondaryValue: parts.join(" · ") || undefined };
}
// Mirrors UsageFormatter.resetCountdownDescription so the row reads like native reset text.
function countdownText(millis) {
const seconds = Math.max(0, millis / 1000);
if (seconds < 1) return "now";
const totalMinutes = Math.max(1, Math.ceil(seconds / 60));
const days = Math.floor(totalMinutes / 1440);
const hours = Math.floor(totalMinutes / 60) % 24;
const minutes = totalMinutes % 60;
if (days > 0) {
if (hours > 0) return `in ${days}d ${hours}h`;
if (minutes > 0) return `in ${days}d ${minutes}m`;
return `in ${days}d`;
}
if (hours > 0) return minutes > 0 ? `in ${hours}h ${minutes}m` : `in ${hours}h`;
return `in ${totalMinutes}m`;
}
// Peak is Mon-Fri 06:00-10:00 UTC (14:00-18:00 UTC+8); weekends are off-peak all day.
// Credit plans charge 1x peak / 0.5x off-peak (docs.z.ai/devpack/overview); legacy
// TOKENS_LIMIT plans charge model-dependent flat rates, so the row is credit-only.
// No z.ai endpoint exposes this - it is purely a function of the injected clock.
function quotaRateRow() {
const PEAK_START = 6;
const PEAK_END = 10;
const now = new Date(ctx.date.nowMillis());
const day = now.getUTCDay();
const hour = now.getUTCHours();
const isPeak = day >= 1 && day <= 5 && hour >= PEAK_START && hour < PEAK_END;
const boundary = new Date(Date.UTC(
now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), isPeak ? PEAK_END : PEAK_START));
if (!isPeak) {
if (hour >= PEAK_START) boundary.setUTCDate(boundary.getUTCDate() + 1);
while (boundary.getUTCDay() === 0 || boundary.getUTCDay() === 6) {
boundary.setUTCDate(boundary.getUTCDate() + 1);
}
}
const countdown = countdownText(boundary.getTime() - now.getTime());
return {
label: "Quota rate",
value: isPeak ? "Peak" : "Off-peak",
secondaryValue: `${isPeak ? "off-peak" : "peak"} ${countdown}`,
};
}

const limits = root.data.limits.map(parseLimit).filter(Boolean);
const tokenLimits = limits.filter(item => item.raw.type === "TOKENS_LIMIT" || item.raw.type === "CREDIT_LIMIT")
Expand All @@ -122,6 +164,8 @@ defineProvider({
}
if (tokenLimit) result.details[0].rows.push(limitRow(tokenLimit.raw.type === "CREDIT_LIMIT" ? "Credit quota" : "Token quota", tokenLimit));
if (sessionLimit) result.details[0].rows.push(limitRow(sessionLimit.raw.type === "CREDIT_LIMIT" ? "Session credit quota" : "Session token quota", sessionLimit));
const hasCreditLimit = [tokenLimit, sessionLimit].some(item => item && item.raw.type === "CREDIT_LIMIT");
if (hasCreditLimit) result.details[0].rows.push(quotaRateRow());
if (timeLimit) {
result.details[0].rows.push(limitRow("MCP quota", timeLimit));
for (const detail of timeLimit.details.slice(0, 20)) {
Expand Down
31 changes: 31 additions & 0 deletions Tests/CodexBarTests/ProviderPluginDetailsParityTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -356,10 +356,41 @@ struct ProviderPluginDetailsParityTests {
Self.section("Quota details", rows: [
Self.row("Credit quota", "10% used", "10000 limit · 9000 remaining"),
Self.row("Session credit quota", "5% used", "2000 limit · 1900 remaining"),
Self.row("Quota rate", "Off-peak", "peak in 2h 21m"),
]),
])
}

@Test
func `zai quota rate row tracks the credit-plan peak schedule`() async throws {
let cases: [(epoch: TimeInterval, value: String, secondary: String)] = [
(1_786_001_400, "Peak", "off-peak in 2h 30m"), // Thursday 07:30 UTC
(1_786_073_946, "Off-peak", "peak in 2h 21m"), // Friday 03:39 UTC
(1_786_143_600, "Off-peak", "peak in 2d 7h"), // Friday 23:00 UTC skips the weekend
(1_786_172_400, "Off-peak", "peak in 1d 23h"), // Saturday 07:00 UTC is off-peak all day
]
for testCase in cases {
let transport = Self.transport { request in
guard request.url?.path.hasSuffix("/quota/limit") == true else {
throw FixtureError.unexpectedURL(request.url)
}
return Self.zaiCreditQuota
}
let script = try await ProviderPluginRuntime(bundledPlugin: "zai", transport: transport)
.fetchUsage(
settings: [
"Z_AI_REGION": "global",
"Z_AI_USAGE_SCOPE": "personal",
],
secrets: ["Z_AI_API_KEY": "fixture-key"],
now: Date(timeIntervalSince1970: testCase.epoch))

let row = try #require(script.details.first?.rows.first { $0.label == "Quota rate" })
#expect(row.value == testCase.value)
#expect(row.secondaryValue == testCase.secondary)
}
}

@Test
func `OpenAI fixture has Swift core parity and stable details`() async throws {
let transport = Self.transport { request in
Expand Down
12 changes: 12 additions & 0 deletions Tests/CodexBarTests/ProviderPluginRuntimeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ struct ProviderPluginRuntimeTests {
#expect(snapshot.primary?.resetsAt == expected)
}

@Test
func `date nowMillis uses the injected fetch clock`() async throws {
let expected = Date(timeIntervalSince1970: 1_800_000_000)
let runtime = try ProviderPluginRuntime(source: Self.plugin(fetchBody: """
return { primary: { usedPercent: 1, resetDescription: String(ctx.date.nowMillis()) } };
"""))

let snapshot = try await runtime.fetchUsage(secrets: ["TEST_KEY": "test-key"], now: expected)

#expect(snapshot.primary?.resetDescription == "1800000000000")
}

@Test
func `context exposes no browser or timer globals`() throws {
let runtime = try ProviderPluginRuntime(source: Self.plugin())
Expand Down
2 changes: 2 additions & 0 deletions docs/plugin-prototype.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ supply standard ECMAScript built-ins, but no browser or Node host environment. T
positive and capped at 24 hours.
- `ctx.date.now()`, `iso(text)`, `unixSeconds(number)`, and `unixMillis(number)` return JavaScript `Date` objects.
`now()` uses the host refresh clock so fixtures and retries share the snapshot timestamp.
- `ctx.date.nowMillis()` returns that same refresh clock as Unix epoch milliseconds for deterministic date arithmetic
(used by the z.ai quota-rate row).
- `ctx.date.nextDailyReset(timeZoneIdentifier, hour)` returns the next wall-clock hour in an IANA time zone, including
DST transitions. Crof uses `America/Chicago` at hour `0`.
- `ctx.jwt.decode(token)` decodes the JSON payload segment without verifying a signature.
Expand Down
2 changes: 2 additions & 0 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ so portable third-party plugins must use the host helpers below instead of ECMA-
24 hours.
- `ctx.date.now()`, `iso(text)`, `unixSeconds(number)`, and `unixMillis(number)` create JavaScript dates. `now()` uses
the host refresh clock.
- `ctx.date.nowMillis()` returns the same host refresh clock as Unix epoch milliseconds — use it for arithmetic that
should stay deterministic under fixture clocks (the z.ai quota-rate row does).
- `ctx.date.nextDailyReset(timeZoneIdentifier, hour)` returns the next wall-clock reset in an IANA time zone.
- `ctx.env.timeZone` is the host's current IANA time-zone identifier; zero-offset GMT aliases are normalized to `UTC`.
- `ctx.format.number(value, options?)`, `usd(value)`, and `monthDay(date)` provide deterministic formatting on both
Expand Down