Skip to content
Closed
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
3 changes: 3 additions & 0 deletions src/chatgpt/app-server-shim/gate-rewrite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ export function unlockRateLimitGate(value: unknown): boolean {
return { cleared, exhausted, blocked };
}
if (!isRecord(node)) return { cleared, exhausted, blocked };
// A usage window at 100%, in either spelling the gate fields come in: `usedPercent` in the
// app-server's JSON-RPC, `used_percent` in the web usage snapshot.
if (typeof node.usedPercent === "number" && node.usedPercent >= 100) exhausted = true;
if (typeof node.used_percent === "number" && node.used_percent >= 100) exhausted = true;
if (node.spendControlReached !== undefined && node.spendControlReached !== null && node.spendControlReached !== false) blocked = true;
if (isRecord(node.spend_control) && node.spend_control.reached === true) blocked = true;
const reachedType = node.rate_limit_reached_type;
Expand Down
3 changes: 2 additions & 1 deletion structure/clients/chatgpt-desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ The pure gate rewrite changes known plain-quota fields only in eligible JSON-RPC
rate-limit notifications and top-level rate-limit results. Workspace, credit,
unknown reached-type and spend-control restrictions preserve closed gate flags.
Both the rate-limit flags and `ordinaryUsageAllowed` open only where the subtree shows
plain-quota evidence: a cleared plain reached type or a usage window at 100%.
plain-quota evidence: a cleared plain reached type or a usage window at 100% (`usedPercent` in the
RPC, `used_percent` in the web usage snapshot; every gate field is read in both spellings).
Usage percentages, resets and window durations remain accurate. Unrelated messages
and malformed lines remain byte-identical; changed lines are reserialized.
A per-line rewrite exception preserves that line. A failure in the framing/rewrite
Expand Down
38 changes: 38 additions & 0 deletions tests/clients/desktop-app-server-shim.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import { rewriteAppServerLine } from "../../src/chatgpt/app-server-shim/app-server-rewrite";
import { unlockRateLimitGate } from "../../src/chatgpt/app-server-shim/gate-rewrite";
import { createRpcLineFilter, runStdoutFilter, runChatgptAppServerFilter } from "../../src/chatgpt/app-server-shim/filter";

/**
Expand Down Expand Up @@ -126,6 +127,43 @@ describe("app-server line rewrite", () => {
});
});

describe("gate rewrite on the web usage snapshot's spelling", () => {
// The rewrite reads every gate field in both spellings (rate_limit/rateLimit,
// limit_reached/limitReached, spend_control/spendControlReached, ...). The usage window must be
// read in both too, or a snake_case snapshot never shows the plain quota as the reason.
const snapshot = (usedPercent: number, spendReached = false) => ({
usage: {
plan_type: "pro",
rate_limit: {
allowed: false,
limit_reached: true,
primary_window: { used_percent: usedPercent, limit_window_seconds: 604800, reset_after_seconds: 205162 },
},
spend_control: { reached: spendReached },
},
});

test("a window at used_percent 100 is plain-quota evidence: the flags open, the window stays as sent", () => {
const value = snapshot(100);
expect(unlockRateLimitGate(value)).toBe(true);
expect(value.usage.rate_limit.allowed).toBe(true);
expect(value.usage.rate_limit.limit_reached).toBe(false);
expect(value.usage.rate_limit.primary_window).toEqual(snapshot(100).usage.rate_limit.primary_window);
});

test("below 100% the payload shows no quota reason, so the flags stay closed", () => {
const value = snapshot(42);
expect(unlockRateLimitGate(value)).toBe(false);
expect(value).toEqual(snapshot(42));
});

test("a reached spend control keeps the flags closed even with the window at 100%", () => {
const value = snapshot(100, true);
expect(unlockRateLimitGate(value)).toBe(false);
expect(value).toEqual(snapshot(100, true));
});
});

describe("app-server line filter", () => {
const collect = (chunks: Uint8Array[], filter = createRpcLineFilter()) => {
const parts: Uint8Array[] = [];
Expand Down
Loading