authorgravatar for jakeleventhal@me.comJake Leventhal <jakeleventhal@me.com> 2026-09-24 21:41:03-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-24 18:41:03-07:00
log91e53e501c9e7eb03f0ea4a5b72221d08eff9c5c
tree3d54c5aa77cab3ba567cb7b45e1d57ff9c25fb6c
parent0cab7d5ab978a8adaee6827f81693249d0dfeee5
signature Signed by PGP key B5690EEEBB952194

fix(server): report the Grok account email so usage limits merge across environments (#12588)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: macroscopeapp[bot] <170038800+macroscopeapp[bot]@users.noreply.github.com>

3 files changed, 170 insertions(+), 87 deletions(-)

apps/server/src/provider/Drivers/GrokDriver.ts+8-3
......@@ -19,7 +19,7 @@ import {
1919 enrichGrokSnapshot,
2020} from "../Layers/GrokProvider.ts";
2121import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
22import { readGrokUsageLimits } from "../Layers/grokUsageLimits.ts";
22import { readGrokAccount } from "../Layers/grokUsageLimits.ts";
2323import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
2424import {
2525 defaultProviderContinuationIdentity,
......@@ -133,8 +133,13 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
133133 const checkProvider = checkGrokProviderStatus(effectiveConfig, processEnv, cwd).pipe(
134134 Effect.flatMap((snapshot) =>
135135 effectiveConfig.enabled && snapshot.installed && snapshot.auth.status === "authenticated"
136 ? readGrokUsageLimits(processEnv).pipe(
137 Effect.map((usageLimits) => ({ ...snapshot, usageLimits })),
136 ? readGrokAccount(processEnv).pipe(
137 // The email lets clients recognize one account signed in on several environments.
138 Effect.map(({ email, usageLimits }) => ({
139 ...snapshot,
140 auth: email ? { ...snapshot.auth, email } : snapshot.auth,
141 usageLimits,
142 })),
138143 )
139144 : Effect.succeed(snapshot),
140145 ),
apps/server/src/provider/Layers/GrokProvider.test.ts+71-12
......@@ -19,7 +19,7 @@ import {
1919 parseGrokModelsCliOutput,
2020} from "./GrokProvider.ts";
2121import { execScriptSource, writeFakeCli } from "../../testUtils/fakeCli.ts";
22import { grokUsageResponseToLimits, readGrokUsageLimits } from "./grokUsageLimits.ts";
22import { grokUsageResponseToLimits, readGrokAccount } from "./grokUsageLimits.ts";
2323
2424const decodeGrokSettings = Schema.decodeSync(GrokSettings);
2525const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url));
......@@ -586,7 +586,7 @@ describe("Grok usage limits", () => {
586586 });
587587});
588588
589it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => {
589it.layer(NodeServices.layer)("readGrokAccount", (it) => {
590590 it.effect("reads the configured Grok home and prefers the current login scope to legacy", () =>
591591 Effect.gen(function* () {
592592 const fs = yield* FileSystem.FileSystem;
......@@ -595,7 +595,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => {
595595 NodePath.join(directory, "auth.json"),
596596 '{"https://auth.x.ai::b1a00492-073a-47ea-816f-4c329264a828":{"key":"session-token","auth_mode":"oauth"},"https://accounts.x.ai/sign-in":{"key":"legacy-token"}}',
597597 );
598 const limits = yield* readGrokUsageLimits({
598 const { usageLimits: limits } = yield* readGrokAccount({
599599 GROK_HOME: directory,
600600 HOME: "/unrelated-home",
601601 }).pipe(
......@@ -621,7 +621,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => {
621621 it.effect(
622622 "uses GROK_AUTH without reading stored credentials and accepts the legacy login scope",
623623 () =>
624 readGrokUsageLimits({
624 readGrokAccount({
625625 GROK_AUTH: '{"https://accounts.x.ai/sign-in":{"key":"legacy-token"}}',
626626 }).pipe(
627627 Effect.provideService(
......@@ -645,7 +645,9 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => {
645645 );
646646 }),
647647 ),
648 Effect.tap((limits) => Effect.sync(() => expect(limits.windows[0]?.usedPercent).toBe(12))),
648 Effect.tap(({ usageLimits }) =>
649 Effect.sync(() => expect(usageLimits.windows[0]?.usedPercent).toBe(12)),
650 ),
649651 ),
650652 );
651653
......@@ -684,7 +686,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => {
684686 GROK_CONFIG_PATH: "/custom-config.toml",
685687 },
686688 ]) {
687 const limits = yield* readGrokUsageLimits({
689 const { usageLimits: limits } = yield* readGrokAccount({
688690 HOME: "/definitely/not/a/grok-home",
689691 ...environment,
690692 }).pipe(
......@@ -706,7 +708,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => {
706708 const fs = yield* FileSystem.FileSystem;
707709 const directory = yield* fs.makeTempDirectoryScoped();
708710 const client = HttpClient.make(() => Effect.die("must not request without credentials"));
709 const missing = yield* readGrokUsageLimits({ HOME: directory }).pipe(
711 const { usageLimits: missing } = yield* readGrokAccount({ HOME: directory }).pipe(
710712 Effect.provideService(HttpClient.HttpClient, client),
711713 );
712714 expect(missing.unavailable?.reason).toBe("unsupported");
......@@ -714,7 +716,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => {
714716 "private-token-invalid-json",
715717 '{"https://accounts.x.ai/sign-in":{"key":42}}',
716718 ]) {
717 const malformed = yield* readGrokUsageLimits({
719 const { usageLimits: malformed } = yield* readGrokAccount({
718720 GROK_HOME: directory,
719721 GROK_AUTH: contents,
720722 }).pipe(Effect.provideService(HttpClient.HttpClient, client));
......@@ -735,7 +737,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => {
735737 '[grok_com_config]\nissuer = "https://custom.example"',
736738 'endpoints.proxy = "https://custom.example"',
737739 ]) {
738 const limits = yield* readGrokUsageLimits({
740 const { usageLimits: limits } = yield* readGrokAccount({
739741 GROK_AUTH: '{"https://accounts.x.ai/sign-in":{"key":"stored-token"}}',
740742 }).pipe(
741743 Effect.provideService(
......@@ -758,15 +760,16 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => {
758760 }),
759761 );
760762
761 it.effect("sanitizes HTTP failures and malformed billing responses", () =>
763 it.effect("sanitizes HTTP failures and malformed billing responses, keeping the account", () =>
762764 Effect.gen(function* () {
763765 for (const response of [
764766 new Response("private response", { status: 401 }),
765767 Response.json({ config: { creditUsagePercent: "private-value" } }),
766768 ]) {
767 const limits = yield* readGrokUsageLimits({
769 const { email, usageLimits: limits } = yield* readGrokAccount({
768770 HOME: "/definitely/not/a/grok-home",
769 GROK_AUTH: '{"https://accounts.x.ai/sign-in":{"key":"private-token"}}',
771 GROK_AUTH:
772 '{"https://accounts.x.ai/sign-in":{"key":"private-token","email":"someone@example.com"}}',
770773 }).pipe(
771774 Effect.provideService(
772775 HttpClient.HttpClient,
......@@ -775,6 +778,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => {
775778 ),
776779 ),
777780 );
781 expect(email).toBe("someone@example.com");
778782 expect(limits.windows).toEqual([]);
779783 expect(limits.unavailable).toEqual({
780784 reason: "probeFailed",
......@@ -784,3 +788,58 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => {
784788 }),
785789 );
786790});
791
792it.layer(NodeServices.layer)("readGrokAccount email", (it) => {
793 it.effect("names the account whose limits were read, and nothing for other auth", () =>
794 Effect.gen(function* () {
795 const current = '"https://auth.x.ai::b1a00492-073a-47ea-816f-4c329264a828"';
796 const cases = [
797 [
798 { GROK_AUTH: `{${current}:{"key":"t","email":" Someone@Example.com "}}` },
799 "Someone@Example.com",
800 ],
801 [
802 {
803 GROK_AUTH: `{${current}:{"key":"t","email":"current@example.com"},"https://accounts.x.ai/sign-in":{"key":"t","email":"legacy@example.com"}}`,
804 },
805 "current@example.com",
806 ],
807 [{ GROK_AUTH: `{${current}:{"key":"t"}}` }, undefined],
808 [
809 {
810 GROK_AUTH: `{${current}:{"key":"t","email":"someone@example.com"}}`,
811 XAI_API_KEY: "api-key",
812 },
813 undefined,
814 ],
815 [
816 {
817 GROK_AUTH: `{${current}:{"key":"t","email":"someone@example.com","auth_mode":"api_key"}}`,
818 },
819 undefined,
820 ],
821 [{ GROK_AUTH: "not-json" }, undefined],
822 ] as const;
823 for (const [environment, expected] of cases) {
824 const { email, usageLimits } = yield* readGrokAccount({
825 HOME: "/definitely/not/a/grok-home",
826 ...environment,
827 }).pipe(
828 Effect.provideService(
829 HttpClient.HttpClient,
830 HttpClient.make((request) =>
831 Effect.succeed(
832 HttpClientResponse.fromWeb(
833 request,
834 Response.json({ config: { creditUsagePercent: 25 } }),
835 ),
836 ),
837 ),
838 ),
839 );
840 expect(email).toBe(expected);
841 if (expected) expect(usageLimits.windows[0]?.usedPercent).toBe(25);
842 }
843 }),
844 );
845});
apps/server/src/provider/Layers/grokUsageLimits.ts+91-72
......@@ -18,6 +18,7 @@ const GrokCredentials = Schema.Record(
1818 Schema.Struct({
1919 key: Schema.optional(Schema.String),
2020 auth_mode: Schema.optional(Schema.String),
21 email: Schema.optional(Schema.String),
2122 }),
2223);
2324const decodeCredentials = Schema.decodeEffect(Schema.fromJsonString(GrokCredentials));
......@@ -65,72 +66,97 @@ export function grokUsageResponseToLimits(
6566 return makeUsageLimits({ checkedAt, windows: [window] });
6667}
6768
68export const readGrokUsageLimits = Effect.fn("readGrokUsageLimits")(function* (
69/**
70 * The grok.com login the CLI uses by default, or undefined when the CLI is
71 * configured to pick another account, endpoint, or an API key.
72 */
73const readGrokCredential = Effect.fn("readGrokCredential")(function* (
74 environment: NodeJS.ProcessEnv,
75) {
76 // T3's ACP adapter explicitly selects API-key auth when this variable is set.
77 if (environment.XAI_API_KEY?.trim()) return undefined;
78 // Alternate auth deployments can select another scope or account from the same file.
79 if (
80 [
81 "GROK_OIDC_ISSUER",
82 "GROK_OIDC_CLIENT_ID",
83 "GROK_OAUTH2_ISSUER",
84 "GROK_OAUTH2_CLIENT_ID",
85 "GROK_OAUTH2_PRINCIPAL_TYPE",
86 "GROK_OAUTH2_PRINCIPAL_ID",
87 "GROK_AUTH_PROVIDER_COMMAND",
88 "GROK_LOCAL_AUTH",
89 "GROK_CLI_CHAT_PROXY_BASE_URL",
90 "GROK_MODELS_BASE_URL",
91 "GROK_CONFIG",
92 "GROK_CONFIG_PATH",
93 ].some((name) => environment[name]?.trim())
94 ) {
95 return undefined;
96 }
97 const fs = yield* FileSystem.FileSystem;
98 const path = yield* Path.Path;
99 const home =
100 environment.GROK_HOME?.trim() ||
101 path.join(environment.HOME || environment.USERPROFILE || NodeOS.homedir(), ".grok");
102 for (const configPath of [
103 path.join(home, "config.toml"),
104 path.join(home, "managed_config.toml"),
105 path.join(home, "requirements.toml"),
106 "/etc/grok/managed_config.toml",
107 "/etc/grok/requirements.toml",
108 ]) {
109 const config = yield* fs.readFileString(configPath).pipe(
110 Effect.catchTags({
111 PlatformError: (error) =>
112 error.reason._tag === "NotFound" ? Effect.succeed("") : Effect.fail(error),
113 }),
114 );
115 // These sections can change the selected account or endpoint. Leave custom deployments to the CLI.
116 if (/^\s*(?:\[\[?\s*)?["']?(?:auth|grok_com_config|endpoints)["']?\s*[.\]=]/m.test(config)) {
117 return undefined;
118 }
119 }
120 const contents =
121 environment.GROK_AUTH?.trim() ||
122 (yield* fs.readFileString(path.join(home, "auth.json")).pipe(
123 Effect.catchTags({
124 PlatformError: (error) =>
125 error.reason._tag === "NotFound" ? Effect.succeed("{}") : Effect.fail(error),
126 }),
127 ));
128 const credentials = yield* decodeCredentials(contents);
129 // Never pick an arbitrary account from other deployments stored in the same file.
130 const credential =
131 credentials["https://auth.x.ai::b1a00492-073a-47ea-816f-4c329264a828"] ??
132 credentials["https://accounts.x.ai/sign-in"];
133 return credential?.auth_mode === "api_key" ? undefined : credential;
134});
135
136/**
137 * Reads the default grok.com login once and reports its usage limits along with
138 * its email, so the email always names the account whose quota was read.
139 */
140export const readGrokAccount = Effect.fn("readGrokAccount")(function* (
69141 environment: NodeJS.ProcessEnv = process.env,
70142) {
71143 const checkedAt = DateTime.formatIso(yield* DateTime.now);
72 return yield* Effect.gen(function* () {
73 // T3's ACP adapter explicitly selects API-key auth when this variable is set.
74 if (environment.XAI_API_KEY?.trim()) {
75 return makeUnavailableUsageLimits({ checkedAt, reason: "unsupported" });
76 }
77 // Alternate auth deployments can select another scope or account from the same file.
78 if (
79 [
80 "GROK_OIDC_ISSUER",
81 "GROK_OIDC_CLIENT_ID",
82 "GROK_OAUTH2_ISSUER",
83 "GROK_OAUTH2_CLIENT_ID",
84 "GROK_OAUTH2_PRINCIPAL_TYPE",
85 "GROK_OAUTH2_PRINCIPAL_ID",
86 "GROK_AUTH_PROVIDER_COMMAND",
87 "GROK_LOCAL_AUTH",
88 "GROK_CLI_CHAT_PROXY_BASE_URL",
89 "GROK_MODELS_BASE_URL",
90 "GROK_CONFIG",
91 "GROK_CONFIG_PATH",
92 ].some((name) => environment[name]?.trim())
93 ) {
94 return makeUnavailableUsageLimits({ checkedAt, reason: "unsupported" });
95 }
96 const fs = yield* FileSystem.FileSystem;
97 const path = yield* Path.Path;
98 const home =
99 environment.GROK_HOME?.trim() ||
100 path.join(environment.HOME || environment.USERPROFILE || NodeOS.homedir(), ".grok");
101 for (const configPath of [
102 path.join(home, "config.toml"),
103 path.join(home, "managed_config.toml"),
104 path.join(home, "requirements.toml"),
105 "/etc/grok/managed_config.toml",
106 "/etc/grok/requirements.toml",
107 ]) {
108 const config = yield* fs.readFileString(configPath).pipe(
109 Effect.catchTags({
110 PlatformError: (error) =>
111 error.reason._tag === "NotFound" ? Effect.succeed("") : Effect.fail(error),
112 }),
113 );
114 // These sections can change the selected account or endpoint. Leave custom deployments to the CLI.
115 if (/^\s*(?:\[\[?\s*)?["']?(?:auth|grok_com_config|endpoints)["']?\s*[.\]=]/m.test(config)) {
116 return makeUnavailableUsageLimits({ checkedAt, reason: "unsupported" });
117 }
118 }
119 const contents =
120 environment.GROK_AUTH?.trim() ||
121 (yield* fs.readFileString(path.join(home, "auth.json")).pipe(
122 Effect.catchTags({
123 PlatformError: (error) =>
124 error.reason._tag === "NotFound" ? Effect.succeed("{}") : Effect.fail(error),
125 }),
126 ));
127 const credentials = yield* decodeCredentials(contents);
128 // Never pick an arbitrary account from other deployments stored in the same file.
129 const credential =
130 credentials["https://auth.x.ai::b1a00492-073a-47ea-816f-4c329264a828"] ??
131 credentials["https://accounts.x.ai/sign-in"];
132 const token = credential?.auth_mode === "api_key" ? undefined : credential?.key?.trim();
133 if (!token) return makeUnavailableUsageLimits({ checkedAt, reason: "unsupported" });
144 const probeFailed = makeUnavailableUsageLimits({
145 checkedAt,
146 reason: "probeFailed",
147 message: "Grok could not read usage limits.",
148 });
149 const credential = yield* Effect.option(
150 readGrokCredential(environment).pipe(Effect.timeout("10 seconds")),
151 );
152 if (Option.isNone(credential)) return { email: undefined, usageLimits: probeFailed };
153 const email = credential.value?.email?.trim() || undefined;
154 const token = credential.value?.key?.trim();
155 if (!token) {
156 return { email, usageLimits: makeUnavailableUsageLimits({ checkedAt, reason: "unsupported" }) };
157 }
158 // A failed quota request still knows which account it asked about.
159 const usageLimits = yield* Effect.gen(function* () {
134160 const client = yield* HttpClient.HttpClient;
135161 const response = yield* client.execute(
136162 HttpClientRequest.get("https://cli-chat-proxy.grok.com/v1/billing?format=credits").pipe(
......@@ -143,14 +169,7 @@ export const readGrokUsageLimits = Effect.fn("readGrokUsageLimits")(function* (
143169 return grokUsageResponseToLimits(body, checkedAt);
144170 }).pipe(
145171 Effect.timeout("10 seconds"),
146 Effect.catch(() =>
147 Effect.succeed(
148 makeUnavailableUsageLimits({
149 checkedAt,
150 reason: "probeFailed",
151 message: "Grok could not read usage limits.",
152 }),
153 ),
154 ),
172 Effect.catch(() => Effect.succeed(probeFailed)),
155173 );
174 return { email, usageLimits };
156175});