1import type { ServerProviderUsageWindow } from "@t3tools/contracts";
2import * as DateTime from "effect/DateTime";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Option from "effect/Option";
6import * as Path from "effect/Path";
7import * as Schema from "effect/Schema";
8import * as HttpClient from "effect/http/HttpClient";
9import * as HttpClientRequest from "effect/http/HttpClientRequest";
10import * as HttpClientResponse from "effect/http/HttpClientResponse";
11import {
12 clampPercent,
13 makeUnavailableUsageLimits,
14 makeUsageLimits,
15} from "@t3tools/provider-core/server/usageLimits";
16import * as HostProcess from "@t3tools/shared/HostProcess";
17
18const GrokCredentials = Schema.Record(
19 Schema.String,
20 Schema.Struct({
21 key: Schema.optional(Schema.String),
22 auth_mode: Schema.optional(Schema.String),
23 email: Schema.optional(Schema.String),
24 }),
25);
26const decodeCredentials = Schema.decodeEffect(Schema.fromJsonString(GrokCredentials));
27const GrokUsageResponse = Schema.Struct({
28 config: Schema.optional(
29 Schema.Struct({
30 creditUsagePercent: Schema.optional(Schema.Number),
31 currentPeriod: Schema.optional(
32 Schema.Struct({
33 type: Schema.optional(Schema.String),
34 end: Schema.optional(Schema.String),
35 }),
36 ),
37 }),
38 ),
39});
40
41export function grokUsageResponseToLimits(
42 response: typeof GrokUsageResponse.Type,
43 checkedAt: string,
44) {
45 const usedPercent = response.config?.creditUsagePercent;
46 if (usedPercent === undefined || !Number.isFinite(usedPercent)) {
47 // A billing read that succeeded but carries no percentage is an account
48 // with nothing metered yet, not one that can never report: xAI omits the
49 // field entirely (rather than sending 0) until usage registers, then fills
50 // it in. Calling that `unsupported` would strand the account — the Limits
51 // view drops unsupported entries and deliberately mutes their notice, so a
52 // freshly signed-in Grok account would vanish with no explanation until it
53 // happened to be used, and `applyUsageLimitsUpdate` would refuse the
54 // mid-turn windows that could have recovered it.
55 return makeUsageLimits({ checkedAt, windows: [] });
56 }
57 const period = response.config?.currentPeriod;
58 const periodType = period?.type?.replace(/^USAGE_PERIOD_TYPE_/, "");
59 const kind = periodType === "WEEKLY" ? "weekly" : periodType === "MONTHLY" ? "monthly" : "other";
60 const reset = period?.end ? DateTime.make(period.end) : Option.none();
61 const window: ServerProviderUsageWindow = {
62 id: "subscription",
63 kind,
64 label: kind === "weekly" ? "Weekly" : kind === "monthly" ? "Monthly" : "Subscription",
65 usedPercent: clampPercent(usedPercent),
66 ...(Option.isSome(reset) ? { resetsAt: DateTime.formatIso(reset.value) } : {}),
67 };
68 return makeUsageLimits({ checkedAt, windows: [window] });
69}
70
71/**
72 * The grok.com login the CLI uses by default, or undefined when the CLI is
73 * configured to pick another account, endpoint, or an API key.
74 */
75const readGrokCredential = Effect.fn("readGrokCredential")(function* (
76 environment: NodeJS.ProcessEnv,
77) {
78 // T3's ACP adapter explicitly selects API-key auth when this variable is set.
79 if (environment.XAI_API_KEY?.trim()) return undefined;
80 // Alternate auth deployments can select another scope or account from the same file.
81 if (
82 [
83 "GROK_OIDC_ISSUER",
84 "GROK_OIDC_CLIENT_ID",
85 "GROK_OAUTH2_ISSUER",
86 "GROK_OAUTH2_CLIENT_ID",
87 "GROK_OAUTH2_PRINCIPAL_TYPE",
88 "GROK_OAUTH2_PRINCIPAL_ID",
89 "GROK_AUTH_PROVIDER_COMMAND",
90 "GROK_LOCAL_AUTH",
91 "GROK_CLI_CHAT_PROXY_BASE_URL",
92 "GROK_MODELS_BASE_URL",
93 "GROK_CONFIG",
94 "GROK_CONFIG_PATH",
95 ].some((name) => environment[name]?.trim())
96 ) {
97 return undefined;
98 }
99 const fs = yield* FileSystem.FileSystem;
100 const path = yield* Path.Path;
101 const home =
102 environment.GROK_HOME?.trim() ||
103 path.join(
104 environment.HOME || environment.USERPROFILE || (yield* HostProcess.HomeDirectory),
105 ".grok",
106 );
107 for (const configPath of [
108 path.join(home, "config.toml"),
109 path.join(home, "managed_config.toml"),
110 path.join(home, "requirements.toml"),
111 "/etc/grok/managed_config.toml",
112 "/etc/grok/requirements.toml",
113 ]) {
114 const config = yield* fs.readFileString(configPath).pipe(
115 Effect.catchTags({
116 PlatformError: (error) =>
117 error.reason._tag === "NotFound" ? Effect.succeed("") : Effect.fail(error),
118 }),
119 );
120 // These sections can change the selected account or endpoint. Leave custom deployments to the CLI.
121 if (/^\s*(?:\[\[?\s*)?["']?(?:auth|grok_com_config|endpoints)["']?\s*[.\]=]/m.test(config)) {
122 return undefined;
123 }
124 }
125 const contents =
126 environment.GROK_AUTH?.trim() ||
127 (yield* fs.readFileString(path.join(home, "auth.json")).pipe(
128 Effect.catchTags({
129 PlatformError: (error) =>
130 error.reason._tag === "NotFound" ? Effect.succeed("{}") : Effect.fail(error),
131 }),
132 ));
133 const credentials = yield* decodeCredentials(contents);
134 // Never pick an arbitrary account from other deployments stored in the same file.
135 const credential =
136 credentials["https://auth.x.ai::b1a00492-073a-47ea-816f-4c329264a828"] ??
137 credentials["https://accounts.x.ai/sign-in"];
138 return credential?.auth_mode === "api_key" ? undefined : credential;
139});
140
141/**
142 * Reads the default grok.com login once and reports its usage limits along with
143 * its email, so the email always names the account whose quota was read.
144 */
145export const readGrokAccount = Effect.fn("readGrokAccount")(function* (
146 environment: NodeJS.ProcessEnv = process.env,
147) {
148 const checkedAt = DateTime.formatIso(yield* DateTime.now);
149 const probeFailed = makeUnavailableUsageLimits({
150 checkedAt,
151 reason: "probeFailed",
152 message: "Grok could not read usage limits.",
153 });
154 const credential = yield* Effect.option(
155 readGrokCredential(environment).pipe(Effect.timeout("10 seconds")),
156 );
157 if (Option.isNone(credential)) return { email: undefined, usageLimits: probeFailed };
158 const email = credential.value?.email?.trim() || undefined;
159 const token = credential.value?.key?.trim();
160 if (!token) {
161 return { email, usageLimits: makeUnavailableUsageLimits({ checkedAt, reason: "unsupported" }) };
162 }
163 // A failed quota request still knows which account it asked about.
164 const usageLimits = yield* Effect.gen(function* () {
165 const client = yield* HttpClient.HttpClient;
166 const response = yield* client.execute(
167 HttpClientRequest.get("https://cli-chat-proxy.grok.com/v1/billing?format=credits").pipe(
168 HttpClientRequest.bearerToken(token),
169 ),
170 );
171 const body = yield* HttpClientResponse.schemaBodyJson(GrokUsageResponse)(
172 yield* HttpClientResponse.filterStatusOk(response),
173 );
174 return grokUsageResponseToLimits(body, checkedAt);
175 }).pipe(
176 Effect.timeout("10 seconds"),
177 Effect.orElseSucceed(() => probeFailed),
178 );
179 return { email, usageLimits };
180});