diff --git a/apps/server/src/usage/cliproxyApi.test.ts b/apps/server/src/usage/cliproxyApi.test.ts index b0fee3638282..d22d283e5496 100644 --- a/apps/server/src/usage/cliproxyApi.test.ts +++ b/apps/server/src/usage/cliproxyApi.test.ts @@ -47,7 +47,12 @@ const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); function fixture( options: { - accounts?: Array<(typeof accounts)[number] & { disabled?: boolean }>; + accounts?: Array< + (typeof accounts)[number] & { + disabled?: boolean; + id_token: { chatgpt_account_id: string; plan_type?: string; chatgpt_plan_type?: string }; + } + >; upstream?: (request: RequestBody) => { status: number; body: unknown }; cooldownStatus?: number; } = {}, @@ -141,6 +146,34 @@ describe("CLIProxyAPI built-in management API", () => { }), ); + it.effect("uses current and legacy token plans for missing Free and Go quota metadata", () => + Effect.gen(function* () { + for (const field of ["plan_type", "chatgpt_plan_type"] as const) { + const test = fixture({ + accounts: accounts.map((account, index) => ({ + ...account, + id_token: { ...account.id_token, [field]: index === 0 ? "free" : "go" }, + })), + upstream: () => ({ + status: 200, + body: { rate_limit: { primary_window: { used_percent: 12 } } }, + }), + }); + const api = yield* test.api; + const result = yield* api.readAccounts(config); + expect(result.map((account) => account.plan)).toEqual([ + "ChatGPT Free Subscription", + "ChatGPT Go Subscription", + ]); + for (const account of result) { + expect(account.usageLimits.windows).toMatchObject([ + { kind: "monthly", windowDurationMins: 43_200, usedPercent: 12 }, + ]); + } + } + }), + ); + it.effect("keeps usage when the credits endpoint fails", () => Effect.gen(function* () { const test = fixture({ diff --git a/apps/server/src/usage/cliproxyApi.ts b/apps/server/src/usage/cliproxyApi.ts index 6bfd3fe4772e..4a09de1f4438 100644 --- a/apps/server/src/usage/cliproxyApi.ts +++ b/apps/server/src/usage/cliproxyApi.ts @@ -27,6 +27,7 @@ const AuthFile = Schema.Struct({ Schema.Struct({ chatgpt_account_id: Schema.optional(Schema.String), chatgpt_plan_type: Schema.optional(Schema.String), + plan_type: Schema.optional(Schema.String), }), ), }); @@ -252,14 +253,16 @@ export const makeCliproxyApi = Effect.gen(function* () { // A credits outage must not hide successfully fetched quota windows. const available = yield* credits(config, account).pipe(Effect.orElseSucceed(() => undefined)); const next = available?.[0]; + const planType = + usage.plan_type ?? account.id_token?.plan_type ?? account.id_token?.chatgpt_plan_type; return { ...base, - plan: codexPlanLabel(usage.plan_type ?? account.id_token?.chatgpt_plan_type), + plan: codexPlanLabel(planType), usageLimits: { ...codexRateLimitsToLimits({ checkedAt, snapshot: { - planType: usage.plan_type ?? null, + planType: planType ?? null, primary: toWindow(usage.rate_limit?.primary_window), secondary: toWindow(usage.rate_limit?.secondary_window), },