1import {
2 ProviderDriverKind,
3 UsageLimitSourceError,
4 type ProviderConsumeResetCreditResult,
5 type UsageLimitSourceAccount,
6 type UsageLimitSourceConfig,
7} from "@t3tools/contracts";
8import * as Crypto from "effect/Crypto";
9import * as DateTime from "effect/DateTime";
10import * as Effect from "effect/Effect";
11import * as Hex from "effect/encoding/Hex";
12import * as Schema from "effect/Schema";
13import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
14
15import { codexPlanLabel } from "../provider/CodexProvider.ts";
16import { codexRateLimitsToLimits } from "../provider/codexUsageLimits.ts";
17import { claudeUsageResponseToLimits } from "../provider/claudeUsageLimits.ts";
18import { makeUnavailableUsageLimits } from "@t3tools/provider-core/server/usageLimits";
19
20const AuthFile = Schema.Struct({
21 id: Schema.String,
22 auth_index: Schema.String,
23 provider: Schema.String,
24 email: Schema.optional(Schema.String),
25 disabled: Schema.optional(Schema.Boolean),
26 id_token: Schema.optional(
27 Schema.Struct({
28 chatgpt_account_id: Schema.optional(Schema.String),
29 chatgpt_plan_type: Schema.optional(Schema.String),
30 }),
31 ),
32});
33const AuthFiles = Schema.Struct({ files: Schema.Array(AuthFile) });
34const ApiResponse = Schema.Struct({ status_code: Schema.Number, body: Schema.String });
35const CodexWindow = Schema.Struct({
36 used_percent: Schema.Number,
37 reset_at: Schema.optional(Schema.NullOr(Schema.Number)),
38 limit_window_seconds: Schema.optional(Schema.Number),
39});
40const CodexUsage = Schema.Struct({
41 plan_type: Schema.optional(Schema.String),
42 rate_limit: Schema.NullOr(
43 Schema.Struct({
44 primary_window: Schema.optional(Schema.NullOr(CodexWindow)),
45 secondary_window: Schema.optional(Schema.NullOr(CodexWindow)),
46 }),
47 ),
48});
49const ClaudeWindow = Schema.Struct({
50 utilization: Schema.Number,
51 resets_at: Schema.NullOr(Schema.String),
52});
53const ClaudeUsage = Schema.Struct({
54 five_hour: Schema.optional(Schema.NullOr(ClaudeWindow)),
55 seven_day: Schema.optional(Schema.NullOr(ClaudeWindow)),
56 limits: Schema.optional(
57 Schema.Array(
58 Schema.Struct({
59 kind: Schema.String,
60 percent: Schema.optional(Schema.NullOr(Schema.Number)),
61 resets_at: Schema.optional(Schema.NullOr(Schema.String)),
62 scope: Schema.optional(
63 Schema.NullOr(
64 Schema.Struct({
65 model: Schema.optional(Schema.NullOr(Schema.Struct({ display_name: Schema.String }))),
66 }),
67 ),
68 ),
69 }),
70 ),
71 ),
72});
73const CreditList = Schema.Struct({
74 credits: Schema.Array(
75 Schema.Struct({
76 id: Schema.String,
77 status: Schema.String,
78 reset_type: Schema.String,
79 expires_at: Schema.String,
80 }),
81 ),
82});
83
84const decodeAuthFiles = Schema.decodeUnknownEffect(AuthFiles);
85const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
86const decodeApiResponse = Schema.decodeUnknownEffect(ApiResponse);
87const decodeCreditList = Schema.decodeUnknownEffect(Schema.fromJsonString(CreditList));
88const decodeClaudeUsage = Schema.decodeUnknownEffect(Schema.fromJsonString(ClaudeUsage));
89const decodeCodexUsage = Schema.decodeUnknownEffect(Schema.fromJsonString(CodexUsage));
90const isUsageLimitSourceError = Schema.is(UsageLimitSourceError);
91const decodeConsumeResponse = Schema.decodeUnknownEffect(
92 Schema.fromJsonString(
93 Schema.Struct({
94 code: Schema.Literals(["reset", "nothing_to_reset", "no_credit", "already_redeemed"]),
95 }),
96 ),
97);
98
99const CODEX_BASE = "https://chatgpt.com/backend-api/wham";
100const CREDIT_URL = `${CODEX_BASE}/rate-limit-reset-credits`;
101
102// 6f1c2a9e-2d4b-4c1e-9a7f-3b8d5e0c1a42
103const CREDIT_REDEEM_NAMESPACE = new Uint8Array([
104 0x6f, 0x1c, 0x2a, 0x9e, 0x2d, 0x4b, 0x4c, 0x1e, 0x9a, 0x7f, 0x3b, 0x8d, 0x5e, 0x0c, 0x1a, 0x42,
105]);
106
107// UUIDv5 per account and credit also deduplicates retries across T3 environments.
108const creditRedeemRequestId = Effect.fn("CliproxyApi.creditRedeemRequestId")(function* (
109 accountId: string,
110 creditId: string,
111) {
112 const crypto = yield* Crypto.Crypto;
113 const name = new TextEncoder().encode(`${accountId}:${creditId}`);
114 const input = new Uint8Array(CREDIT_REDEEM_NAMESPACE.length + name.length);
115 input.set(CREDIT_REDEEM_NAMESPACE);
116 input.set(name, CREDIT_REDEEM_NAMESPACE.length);
117 const bytes = (yield* crypto.digest("SHA-1", input).pipe(Effect.orDie)).slice(0, 16);
118 bytes[6] = (bytes[6]! & 0x0f) | 0x50;
119 bytes[8] = (bytes[8]! & 0x3f) | 0x80;
120 const hex = Hex.encode(bytes);
121 return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
122});
123
124export const makeCliproxyApi = Effect.gen(function* () {
125 const client = yield* HttpClient.HttpClient;
126 const crypto = yield* Crypto.Crypto;
127
128 const management = Effect.fn("CliproxyApi.management")(function* (
129 config: UsageLimitSourceConfig,
130 path: string,
131 body?: unknown,
132 ) {
133 const url = yield* Effect.try({
134 try: () => new URL(`/v0/management/${path}`, config.url).toString(),
135 catch: () => new UsageLimitSourceError({ detail: "The hub URL is not valid." }),
136 });
137 const request = (
138 body === undefined ? HttpClientRequest.get(url) : HttpClientRequest.post(url)
139 ).pipe(HttpClientRequest.setHeader("Authorization", `Bearer ${config.managementKey}`));
140 const response = yield* client
141 .execute(body === undefined ? request : request.pipe(HttpClientRequest.bodyJsonUnsafe(body)))
142 .pipe(
143 Effect.flatMap(HttpClientResponse.filterStatusOk),
144 Effect.flatMap((response) => response.json),
145 Effect.timeout("15 seconds"),
146 Effect.mapError(
147 () => new UsageLimitSourceError({ detail: "The hub management request failed." }),
148 ),
149 );
150 return response;
151 });
152
153 const authFiles = Effect.fn("CliproxyApi.authFiles")(function* (config: UsageLimitSourceConfig) {
154 const response = yield* management(config, "auth-files");
155 return (yield* decodeAuthFiles(response)).files;
156 });
157
158 const apiCall = Effect.fn("CliproxyApi.apiCall")(function* (
159 config: UsageLimitSourceConfig,
160 account: typeof AuthFile.Type,
161 url: string,
162 data?: unknown,
163 ) {
164 const header =
165 account.provider === "codex"
166 ? {
167 Authorization: "Bearer $TOKEN$",
168 "Content-Type": "application/json",
169 "OpenAI-Beta": "codex-1",
170 Originator: "Codex Desktop",
171 ...(account.id_token?.chatgpt_account_id
172 ? { "Chatgpt-Account-Id": account.id_token.chatgpt_account_id }
173 : {}),
174 }
175 : { Authorization: "Bearer $TOKEN$", "anthropic-beta": "oauth-2025-04-20" };
176 const raw = yield* management(config, "api-call", {
177 auth_index: account.auth_index,
178 method: data === undefined ? "GET" : "POST",
179 url,
180 header,
181 ...(data === undefined ? {} : { data: yield* encodeJson(data) }),
182 });
183 const response = yield* decodeApiResponse(raw);
184 if (response.status_code < 200 || response.status_code >= 300) {
185 return yield* new UsageLimitSourceError({
186 detail: `The provider refused the hub request (HTTP ${response.status_code}).`,
187 });
188 }
189 return response.body;
190 });
191
192 const credits = Effect.fn("CliproxyApi.credits")(function* (
193 config: UsageLimitSourceConfig,
194 account: typeof AuthFile.Type,
195 ) {
196 const body = yield* apiCall(config, account, CREDIT_URL);
197 const response = yield* decodeCreditList(body);
198 const now = DateTime.toEpochMillis(yield* DateTime.now);
199 return response.credits
200 .filter(
201 (credit) =>
202 credit.reset_type === "codex_rate_limits" &&
203 credit.status === "available" &&
204 Date.parse(credit.expires_at) > now,
205 )
206 .toSorted((a, b) => Date.parse(a.expires_at) - Date.parse(b.expires_at));
207 });
208
209 const readAccount = Effect.fn("CliproxyApi.readAccount")(function* (
210 config: UsageLimitSourceConfig,
211 account: typeof AuthFile.Type,
212 ) {
213 const checkedAt = DateTime.formatIso(yield* DateTime.now);
214 const base = {
215 id: account.id,
216 driver: ProviderDriverKind.make(account.provider === "codex" ? "codex" : "claudeAgent"),
217 ...(account.email ? { email: account.email } : {}),
218 ...(account.id_token?.chatgpt_account_id?.trim()
219 ? { workspaceId: account.id_token.chatgpt_account_id.trim() }
220 : {}),
221 };
222 const read = Effect.gen(function* () {
223 if (account.provider === "claude") {
224 const body = yield* apiCall(config, account, "https://api.anthropic.com/api/oauth/usage");
225 const usage = yield* decodeClaudeUsage(body);
226 const model_scoped = (usage.limits ?? []).flatMap((limit) =>
227 limit.kind === "weekly_scoped" && limit.scope?.model && typeof limit.percent === "number"
228 ? [
229 {
230 display_name: limit.scope.model.display_name,
231 utilization: limit.percent,
232 resets_at: limit.resets_at ?? null,
233 },
234 ]
235 : [],
236 );
237 return {
238 ...base,
239 plan: "Claude Subscription",
240 usageLimits: claudeUsageResponseToLimits({
241 checkedAt,
242 response: {
243 rate_limits_available: true,
244 rate_limits: {
245 five_hour: usage.five_hour ?? null,
246 seven_day: usage.seven_day ?? null,
247 model_scoped,
248 },
249 },
250 }).limits,
251 };
252 }
253 const body = yield* apiCall(config, account, `${CODEX_BASE}/usage`);
254 const usage = yield* decodeCodexUsage(body);
255 const toWindow = (window: typeof CodexWindow.Type | null | undefined) =>
256 window
257 ? {
258 usedPercent: window.used_percent,
259 resetsAt: window.reset_at ?? null,
260 ...(window.limit_window_seconds === undefined
261 ? {}
262 : { windowDurationMins: window.limit_window_seconds / 60 }),
263 }
264 : null;
265 // A credits outage must not hide successfully fetched quota windows.
266 const available = yield* credits(config, account).pipe(Effect.orElseSucceed(() => undefined));
267 const next = available?.[0];
268 return {
269 ...base,
270 plan: codexPlanLabel(usage.plan_type ?? account.id_token?.chatgpt_plan_type),
271 usageLimits: {
272 ...codexRateLimitsToLimits({
273 checkedAt,
274 snapshot: {
275 planType: usage.plan_type ?? null,
276 primary: toWindow(usage.rate_limit?.primary_window),
277 secondary: toWindow(usage.rate_limit?.secondary_window),
278 },
279 }),
280 ...(available
281 ? {
282 resetCredits: {
283 availableCount: available.length,
284 ...(next
285 ? {
286 nextCreditId: next.id,
287 nextExpiresAt: DateTime.formatIso(DateTime.makeUnsafe(next.expires_at)),
288 }
289 : {}),
290 },
291 }
292 : {}),
293 },
294 };
295 });
296 return yield* read.pipe(
297 Effect.orElseSucceed(() => ({
298 ...base,
299 usageLimits: makeUnavailableUsageLimits({
300 checkedAt,
301 reason: "probeFailed",
302 message: "The hub could not read this account's usage.",
303 }),
304 })),
305 );
306 });
307
308 const readAccounts = Effect.fn("CliproxyApi.readAccounts")(function* (
309 config: UsageLimitSourceConfig,
310 ): Effect.fn.Return<ReadonlyArray<UsageLimitSourceAccount>, UsageLimitSourceError> {
311 const accounts = yield* authFiles(config).pipe(
312 Effect.mapError(
313 () => new UsageLimitSourceError({ detail: "The hub could not list accounts." }),
314 ),
315 );
316 return yield* Effect.forEach(
317 accounts.filter(
318 (account) =>
319 !account.disabled && (account.provider === "codex" || account.provider === "claude"),
320 ),
321 (account) => readAccount(config, account),
322 { concurrency: 4 },
323 );
324 });
325
326 const consume = Effect.fn("CliproxyApi.consume")(function* (
327 config: UsageLimitSourceConfig,
328 accountId: string,
329 creditId: string,
330 ): Effect.fn.Return<ProviderConsumeResetCreditResult, UsageLimitSourceError> {
331 const operation = Effect.gen(function* () {
332 const account = (yield* authFiles(config)).find((account) => account.id === accountId);
333 if (!account || account.disabled || account.provider !== "codex") {
334 return yield* new UsageLimitSourceError({
335 detail: "The Codex hub account is missing or disabled.",
336 });
337 }
338 const body = yield* apiCall(config, account, `${CREDIT_URL}/consume`, {
339 redeem_request_id: yield* creditRedeemRequestId(
340 account.id_token?.chatgpt_account_id ?? account.id,
341 creditId,
342 ).pipe(Effect.provideService(Crypto.Crypto, crypto)),
343 credit_id: creditId,
344 });
345 const response = yield* decodeConsumeResponse(body);
346 const outcome = (
347 {
348 reset: "reset",
349 nothing_to_reset: "nothingToReset",
350 no_credit: "noCredit",
351 already_redeemed: "alreadyRedeemed",
352 } as const
353 )[response.code];
354 if (outcome !== "reset" && outcome !== "alreadyRedeemed") return { outcome };
355 const cleared = yield* management(config, "reset-quota", {
356 auth_index: account.auth_index,
357 }).pipe(Effect.result);
358 return {
359 outcome,
360 ...(cleared._tag === "Failure"
361 ? {
362 warning:
363 "Credit redeemed, but the hub cooldown could not be cleared. Routing may resume after its cooldown expires.",
364 }
365 : {}),
366 } as const;
367 });
368 return yield* operation.pipe(
369 Effect.mapError((error) =>
370 isUsageLimitSourceError(error)
371 ? error
372 : new UsageLimitSourceError({
373 detail: "The hub returned an unexpected reset-credit response.",
374 }),
375 ),
376 );
377 });
378 return { readAccounts, consume };
379});