212e05dbbb
- Added Codex reset-credit models, endpoints, and redemption methods. - Added reset-credit usage data and cache invalidation for successful redemption. - Added `/reset-usage` slash command and interactive account selector flow. - Added contract tests for reset-credit listing, fallback, and consume outcomes.
153 lines
5.8 KiB
TypeScript
153 lines
5.8 KiB
TypeScript
/**
|
|
* Codex usage parser regressions. The widget client (osx-widgets) keys spark
|
|
* detection off `limit.id.includes("spark")`, so the parser MUST surface
|
|
* `additional_rate_limits[].metered_feature == "codex_bengalfox"` (the upstream
|
|
* codename for GPT-5.3-Codex-Spark) as separate `UsageLimit` entries with
|
|
* `spark` in the id. If this contract breaks, both the TUI and the macOS
|
|
* widget lose per-model visibility.
|
|
*/
|
|
import { describe, expect, it } from "bun:test";
|
|
import type { FetchImpl } from "@oh-my-pi/pi-ai/types";
|
|
import { openaiCodexUsageProvider } from "@oh-my-pi/pi-ai/usage/openai-codex";
|
|
|
|
const accessTokenFixture = (() => {
|
|
const header = Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" })).toString("base64url");
|
|
const body = Buffer.from(
|
|
JSON.stringify({
|
|
"https://api.openai.com/auth": { chatgpt_account_id: "acct-fixture" },
|
|
"https://api.openai.com/profile": { email: "fixture@example.com" },
|
|
}),
|
|
).toString("base64url");
|
|
return `${header}.${body}.sig`;
|
|
})();
|
|
|
|
function makePayload() {
|
|
return {
|
|
plan_type: "pro",
|
|
rate_limit: {
|
|
allowed: true,
|
|
limit_reached: false,
|
|
primary_window: { used_percent: 4, limit_window_seconds: 17940, reset_at: 2_000_000_000 },
|
|
secondary_window: { used_percent: 1, limit_window_seconds: 604740, reset_at: 2_000_500_000 },
|
|
},
|
|
additional_rate_limits: [
|
|
{
|
|
limit_name: "GPT-5.3-Codex-Spark",
|
|
metered_feature: "codex_bengalfox",
|
|
rate_limit: {
|
|
allowed: true,
|
|
limit_reached: false,
|
|
primary_window: { used_percent: 17, limit_window_seconds: 18000, reset_at: 2_000_001_000 },
|
|
secondary_window: { used_percent: 61, limit_window_seconds: 604800, reset_at: 2_000_600_000 },
|
|
},
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
function fakeFetch(payload: unknown): FetchImpl {
|
|
const fn = async () =>
|
|
new Response(JSON.stringify(payload), { status: 200, headers: { "content-type": "application/json" } });
|
|
return fn as unknown as typeof fetch;
|
|
}
|
|
|
|
describe("openai-codex usage parser", () => {
|
|
it("emits primary + secondary limits from the main rate_limit block", async () => {
|
|
const report = await openaiCodexUsageProvider.fetchUsage(
|
|
{
|
|
provider: "openai-codex",
|
|
credential: { type: "oauth", accessToken: accessTokenFixture, accountId: "acct-1", email: "u@example.com" },
|
|
},
|
|
{ fetch: fakeFetch(makePayload()) },
|
|
);
|
|
expect(report).not.toBeNull();
|
|
const main = report?.limits.filter(l => l.id === "openai-codex:primary" || l.id === "openai-codex:secondary");
|
|
expect(main?.map(l => l.id)).toEqual(["openai-codex:primary", "openai-codex:secondary"]);
|
|
expect(main?.[0].scope.tier).toBe("pro");
|
|
expect(main?.[0].amount.usedFraction).toBeCloseTo(0.04, 5);
|
|
});
|
|
|
|
it("surfaces additional_rate_limits as spark UsageLimit entries the widget can detect", async () => {
|
|
const report = await openaiCodexUsageProvider.fetchUsage(
|
|
{
|
|
provider: "openai-codex",
|
|
credential: { type: "oauth", accessToken: accessTokenFixture, accountId: "acct-1", email: "u@example.com" },
|
|
},
|
|
{ fetch: fakeFetch(makePayload()) },
|
|
);
|
|
const spark = report?.limits.filter(l => l.id.includes("spark"));
|
|
expect(spark?.map(l => l.id)).toEqual(["openai-codex:spark:primary", "openai-codex:spark:secondary"]);
|
|
expect(spark?.[0].label).toBe("5 hours (Spark)");
|
|
expect(spark?.[1].label).toBe("7 days (Spark)");
|
|
expect(spark?.[0].scope.tier).toBe("spark");
|
|
expect(spark?.[0].scope.modelId).toBe("GPT-5.3-Codex-Spark");
|
|
expect(spark?.[0].amount.usedFraction).toBeCloseTo(0.17, 5);
|
|
expect(spark?.[1].amount.usedFraction).toBeCloseTo(0.61, 5);
|
|
});
|
|
|
|
it("treats bengalfox codename as spark even without explicit limit_name", async () => {
|
|
const payload = makePayload();
|
|
payload.additional_rate_limits[0].limit_name = undefined as unknown as string;
|
|
const report = await openaiCodexUsageProvider.fetchUsage(
|
|
{
|
|
provider: "openai-codex",
|
|
credential: { type: "oauth", accessToken: accessTokenFixture, accountId: "acct-1", email: "u@example.com" },
|
|
},
|
|
{ fetch: fakeFetch(payload) },
|
|
);
|
|
const spark = report?.limits.find(l => l.id === "openai-codex:spark:primary");
|
|
expect(spark).toBeTruthy();
|
|
expect(spark?.scope.tier).toBe("spark");
|
|
});
|
|
|
|
it("returns a report even when only additional_rate_limits are present (no main rate_limit)", async () => {
|
|
const payload = {
|
|
plan_type: "pro",
|
|
rate_limit: null,
|
|
additional_rate_limits: [
|
|
{
|
|
limit_name: "GPT-5.3-Codex-Spark",
|
|
metered_feature: "codex_bengalfox",
|
|
rate_limit: {
|
|
allowed: true,
|
|
limit_reached: false,
|
|
primary_window: { used_percent: 5, limit_window_seconds: 18000, reset_at: 2_000_000_000 },
|
|
},
|
|
},
|
|
],
|
|
};
|
|
const report = await openaiCodexUsageProvider.fetchUsage(
|
|
{
|
|
provider: "openai-codex",
|
|
credential: { type: "oauth", accessToken: accessTokenFixture, accountId: "acct-1", email: "u@example.com" },
|
|
},
|
|
{ fetch: fakeFetch(payload) },
|
|
);
|
|
expect(report).not.toBeNull();
|
|
expect(report?.limits.map(l => l.id)).toEqual(["openai-codex:spark:primary"]);
|
|
});
|
|
|
|
it("surfaces rate_limit_reset_credits.available_count as report.resetCredits", async () => {
|
|
const payload = { ...makePayload(), rate_limit_reset_credits: { available_count: 1 } };
|
|
const report = await openaiCodexUsageProvider.fetchUsage(
|
|
{
|
|
provider: "openai-codex",
|
|
credential: { type: "oauth", accessToken: accessTokenFixture, accountId: "acct-1", email: "u@example.com" },
|
|
},
|
|
{ fetch: fakeFetch(payload) },
|
|
);
|
|
expect(report?.resetCredits).toEqual({ availableCount: 1 });
|
|
});
|
|
|
|
it("omits resetCredits when the account has no saved resets block", async () => {
|
|
const report = await openaiCodexUsageProvider.fetchUsage(
|
|
{
|
|
provider: "openai-codex",
|
|
credential: { type: "oauth", accessToken: accessTokenFixture, accountId: "acct-1", email: "u@example.com" },
|
|
},
|
|
{ fetch: fakeFetch(makePayload()) },
|
|
);
|
|
expect(report?.resetCredits).toBeUndefined();
|
|
});
|
|
});
|