| author | |
| committer | |
| log | 91e53e501c9e7eb03f0ea4a5b72221d08eff9c5c |
| tree | 3d54c5aa77cab3ba567cb7b45e1d57ff9c25fb6c |
| parent | 0cab7d5ab978a8adaee6827f81693249d0dfeee5 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 { | ... | @@ -19,7 +19,7 @@ import { |
| 19 | enrichGrokSnapshot, | 19 | enrichGrokSnapshot, |
| 20 | } from "../Layers/GrokProvider.ts"; | 20 | } from "../Layers/GrokProvider.ts"; |
| 21 | import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; | 21 | import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; |
| 22 | import { readGrokUsageLimits } from "../Layers/grokUsageLimits.ts"; | 22 | import { readGrokAccount } from "../Layers/grokUsageLimits.ts"; |
| 23 | import { makeManagedServerProvider } from "../makeManagedServerProvider.ts"; | 23 | import { makeManagedServerProvider } from "../makeManagedServerProvider.ts"; |
| 24 | import { | 24 | import { |
| 25 | defaultProviderContinuationIdentity, | 25 | defaultProviderContinuationIdentity, |
| ... | @@ -133,8 +133,13 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = { | ... | @@ -133,8 +133,13 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = { |
| 133 | const checkProvider = checkGrokProviderStatus(effectiveConfig, processEnv, cwd).pipe( | 133 | const checkProvider = checkGrokProviderStatus(effectiveConfig, processEnv, cwd).pipe( |
| 134 | Effect.flatMap((snapshot) => | 134 | Effect.flatMap((snapshot) => |
| 135 | effectiveConfig.enabled && snapshot.installed && snapshot.auth.status === "authenticated" | 135 | effectiveConfig.enabled && snapshot.installed && snapshot.auth.status === "authenticated" |
| 136 | ? readGrokUsageLimits(processEnv).pipe( | 136 | ? readGrokAccount(processEnv).pipe( |
| 137 | Effect.map((usageLimits) => ({ ...snapshot, usageLimits })), | 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 | })), | ||
| 138 | ) | 143 | ) |
| 139 | : Effect.succeed(snapshot), | 144 | : Effect.succeed(snapshot), |
| 140 | ), | 145 | ), |
apps/server/src/provider/Layers/GrokProvider.test.ts+71-12| ... | @@ -19,7 +19,7 @@ import { | ... | @@ -19,7 +19,7 @@ import { |
| 19 | parseGrokModelsCliOutput, | 19 | parseGrokModelsCliOutput, |
| 20 | } from "./GrokProvider.ts"; | 20 | } from "./GrokProvider.ts"; |
| 21 | import { execScriptSource, writeFakeCli } from "../../testUtils/fakeCli.ts"; | 21 | import { execScriptSource, writeFakeCli } from "../../testUtils/fakeCli.ts"; |
| 22 | import { grokUsageResponseToLimits, readGrokUsageLimits } from "./grokUsageLimits.ts"; | 22 | import { grokUsageResponseToLimits, readGrokAccount } from "./grokUsageLimits.ts"; |
| 23 | 23 | ||
| 24 | const decodeGrokSettings = Schema.decodeSync(GrokSettings); | 24 | const decodeGrokSettings = Schema.decodeSync(GrokSettings); |
| 25 | const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)); | 25 | const __dirname = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)); |
| ... | @@ -586,7 +586,7 @@ describe("Grok usage limits", () => { | ... | @@ -586,7 +586,7 @@ describe("Grok usage limits", () => { |
| 586 | }); | 586 | }); |
| 587 | }); | 587 | }); |
| 588 | 588 | ||
| 589 | it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { | 589 | it.layer(NodeServices.layer)("readGrokAccount", (it) => { |
| 590 | it.effect("reads the configured Grok home and prefers the current login scope to legacy", () => | 590 | it.effect("reads the configured Grok home and prefers the current login scope to legacy", () => |
| 591 | Effect.gen(function* () { | 591 | Effect.gen(function* () { |
| 592 | const fs = yield* FileSystem.FileSystem; | 592 | const fs = yield* FileSystem.FileSystem; |
| ... | @@ -595,7 +595,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { | ... | @@ -595,7 +595,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { |
| 595 | NodePath.join(directory, "auth.json"), | 595 | NodePath.join(directory, "auth.json"), |
| 596 | '{"https://auth.x.ai::b1a00492-073a-47ea-816f-4c329264a828":{"key":"session-token","auth_mode":"oauth"},"https://accounts.x.ai/sign-in":{"key":"legacy-token"}}', | 596 | '{"https://auth.x.ai::b1a00492-073a-47ea-816f-4c329264a828":{"key":"session-token","auth_mode":"oauth"},"https://accounts.x.ai/sign-in":{"key":"legacy-token"}}', |
| 597 | ); | 597 | ); |
| 598 | const limits = yield* readGrokUsageLimits({ | 598 | const { usageLimits: limits } = yield* readGrokAccount({ |
| 599 | GROK_HOME: directory, | 599 | GROK_HOME: directory, |
| 600 | HOME: "/unrelated-home", | 600 | HOME: "/unrelated-home", |
| 601 | }).pipe( | 601 | }).pipe( |
| ... | @@ -621,7 +621,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { | ... | @@ -621,7 +621,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { |
| 621 | it.effect( | 621 | it.effect( |
| 622 | "uses GROK_AUTH without reading stored credentials and accepts the legacy login scope", | 622 | "uses GROK_AUTH without reading stored credentials and accepts the legacy login scope", |
| 623 | () => | 623 | () => |
| 624 | readGrokUsageLimits({ | 624 | readGrokAccount({ |
| 625 | GROK_AUTH: '{"https://accounts.x.ai/sign-in":{"key":"legacy-token"}}', | 625 | GROK_AUTH: '{"https://accounts.x.ai/sign-in":{"key":"legacy-token"}}', |
| 626 | }).pipe( | 626 | }).pipe( |
| 627 | Effect.provideService( | 627 | Effect.provideService( |
| ... | @@ -645,7 +645,9 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { | ... | @@ -645,7 +645,9 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { |
| 645 | ); | 645 | ); |
| 646 | }), | 646 | }), |
| 647 | ), | 647 | ), |
| 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 | ), | ||
| 649 | ), | 651 | ), |
| 650 | ); | 652 | ); |
| 651 | 653 | ||
| ... | @@ -684,7 +686,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { | ... | @@ -684,7 +686,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { |
| 684 | GROK_CONFIG_PATH: "/custom-config.toml", | 686 | GROK_CONFIG_PATH: "/custom-config.toml", |
| 685 | }, | 687 | }, |
| 686 | ]) { | 688 | ]) { |
| 687 | const limits = yield* readGrokUsageLimits({ | 689 | const { usageLimits: limits } = yield* readGrokAccount({ |
| 688 | HOME: "/definitely/not/a/grok-home", | 690 | HOME: "/definitely/not/a/grok-home", |
| 689 | ...environment, | 691 | ...environment, |
| 690 | }).pipe( | 692 | }).pipe( |
| ... | @@ -706,7 +708,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { | ... | @@ -706,7 +708,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { |
| 706 | const fs = yield* FileSystem.FileSystem; | 708 | const fs = yield* FileSystem.FileSystem; |
| 707 | const directory = yield* fs.makeTempDirectoryScoped(); | 709 | const directory = yield* fs.makeTempDirectoryScoped(); |
| 708 | const client = HttpClient.make(() => Effect.die("must not request without credentials")); | 710 | 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( |
| 710 | Effect.provideService(HttpClient.HttpClient, client), | 712 | Effect.provideService(HttpClient.HttpClient, client), |
| 711 | ); | 713 | ); |
| 712 | expect(missing.unavailable?.reason).toBe("unsupported"); | 714 | expect(missing.unavailable?.reason).toBe("unsupported"); |
| ... | @@ -714,7 +716,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { | ... | @@ -714,7 +716,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { |
| 714 | "private-token-invalid-json", | 716 | "private-token-invalid-json", |
| 715 | '{"https://accounts.x.ai/sign-in":{"key":42}}', | 717 | '{"https://accounts.x.ai/sign-in":{"key":42}}', |
| 716 | ]) { | 718 | ]) { |
| 717 | const malformed = yield* readGrokUsageLimits({ | 719 | const { usageLimits: malformed } = yield* readGrokAccount({ |
| 718 | GROK_HOME: directory, | 720 | GROK_HOME: directory, |
| 719 | GROK_AUTH: contents, | 721 | GROK_AUTH: contents, |
| 720 | }).pipe(Effect.provideService(HttpClient.HttpClient, client)); | 722 | }).pipe(Effect.provideService(HttpClient.HttpClient, client)); |
| ... | @@ -735,7 +737,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { | ... | @@ -735,7 +737,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { |
| 735 | '[grok_com_config]\nissuer = "https://custom.example"', | 737 | '[grok_com_config]\nissuer = "https://custom.example"', |
| 736 | 'endpoints.proxy = "https://custom.example"', | 738 | 'endpoints.proxy = "https://custom.example"', |
| 737 | ]) { | 739 | ]) { |
| 738 | const limits = yield* readGrokUsageLimits({ | 740 | const { usageLimits: limits } = yield* readGrokAccount({ |
| 739 | GROK_AUTH: '{"https://accounts.x.ai/sign-in":{"key":"stored-token"}}', | 741 | GROK_AUTH: '{"https://accounts.x.ai/sign-in":{"key":"stored-token"}}', |
| 740 | }).pipe( | 742 | }).pipe( |
| 741 | Effect.provideService( | 743 | Effect.provideService( |
| ... | @@ -758,15 +760,16 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { | ... | @@ -758,15 +760,16 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { |
| 758 | }), | 760 | }), |
| 759 | ); | 761 | ); |
| 760 | 762 | ||
| 761 | it.effect("sanitizes HTTP failures and malformed billing responses", () => | 763 | it.effect("sanitizes HTTP failures and malformed billing responses, keeping the account", () => |
| 762 | Effect.gen(function* () { | 764 | Effect.gen(function* () { |
| 763 | for (const response of [ | 765 | for (const response of [ |
| 764 | new Response("private response", { status: 401 }), | 766 | new Response("private response", { status: 401 }), |
| 765 | Response.json({ config: { creditUsagePercent: "private-value" } }), | 767 | Response.json({ config: { creditUsagePercent: "private-value" } }), |
| 766 | ]) { | 768 | ]) { |
| 767 | const limits = yield* readGrokUsageLimits({ | 769 | const { email, usageLimits: limits } = yield* readGrokAccount({ |
| 768 | HOME: "/definitely/not/a/grok-home", | 770 | 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"}}', | ||
| 770 | }).pipe( | 773 | }).pipe( |
| 771 | Effect.provideService( | 774 | Effect.provideService( |
| 772 | HttpClient.HttpClient, | 775 | HttpClient.HttpClient, |
| ... | @@ -775,6 +778,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { | ... | @@ -775,6 +778,7 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { |
| 775 | ), | 778 | ), |
| 776 | ), | 779 | ), |
| 777 | ); | 780 | ); |
| 781 | expect(email).toBe("someone@example.com"); | ||
| 778 | expect(limits.windows).toEqual([]); | 782 | expect(limits.windows).toEqual([]); |
| 779 | expect(limits.unavailable).toEqual({ | 783 | expect(limits.unavailable).toEqual({ |
| 780 | reason: "probeFailed", | 784 | reason: "probeFailed", |
| ... | @@ -784,3 +788,58 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { | ... | @@ -784,3 +788,58 @@ it.layer(NodeServices.layer)("readGrokUsageLimits", (it) => { |
| 784 | }), | 788 | }), |
| 785 | ); | 789 | ); |
| 786 | }); | 790 | }); |
| 791 | |||
| 792 | it.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( | ... | @@ -18,6 +18,7 @@ const GrokCredentials = Schema.Record( |
| 18 | Schema.Struct({ | 18 | Schema.Struct({ |
| 19 | key: Schema.optional(Schema.String), | 19 | key: Schema.optional(Schema.String), |
| 20 | auth_mode: Schema.optional(Schema.String), | 20 | auth_mode: Schema.optional(Schema.String), |
| 21 | email: Schema.optional(Schema.String), | ||
| 21 | }), | 22 | }), |
| 22 | ); | 23 | ); |
| 23 | const decodeCredentials = Schema.decodeEffect(Schema.fromJsonString(GrokCredentials)); | 24 | const decodeCredentials = Schema.decodeEffect(Schema.fromJsonString(GrokCredentials)); |
| ... | @@ -65,72 +66,97 @@ export function grokUsageResponseToLimits( | ... | @@ -65,72 +66,97 @@ export function grokUsageResponseToLimits( |
| 65 | return makeUsageLimits({ checkedAt, windows: [window] }); | 66 | return makeUsageLimits({ checkedAt, windows: [window] }); |
| 66 | } | 67 | } |
| 67 | 68 | ||
| 68 | export 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 | */ | ||
| 73 | const 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 | */ | ||
| 140 | export const readGrokAccount = Effect.fn("readGrokAccount")(function* ( | ||
| 69 | environment: NodeJS.ProcessEnv = process.env, | 141 | environment: NodeJS.ProcessEnv = process.env, |
| 70 | ) { | 142 | ) { |
| 71 | const checkedAt = DateTime.formatIso(yield* DateTime.now); | 143 | const checkedAt = DateTime.formatIso(yield* DateTime.now); |
| 72 | return yield* Effect.gen(function* () { | 144 | const probeFailed = makeUnavailableUsageLimits({ |
| 73 | // T3's ACP adapter explicitly selects API-key auth when this variable is set. | 145 | checkedAt, |
| 74 | if (environment.XAI_API_KEY?.trim()) { | 146 | reason: "probeFailed", |
| 75 | return makeUnavailableUsageLimits({ checkedAt, reason: "unsupported" }); | 147 | message: "Grok could not read usage limits.", |
| 76 | } | 148 | }); |
| 77 | // Alternate auth deployments can select another scope or account from the same file. | 149 | const credential = yield* Effect.option( |
| 78 | if ( | 150 | readGrokCredential(environment).pipe(Effect.timeout("10 seconds")), |
| 79 | [ | 151 | ); |
| 80 | "GROK_OIDC_ISSUER", | 152 | if (Option.isNone(credential)) return { email: undefined, usageLimits: probeFailed }; |
| 81 | "GROK_OIDC_CLIENT_ID", | 153 | const email = credential.value?.email?.trim() || undefined; |
| 82 | "GROK_OAUTH2_ISSUER", | 154 | const token = credential.value?.key?.trim(); |
| 83 | "GROK_OAUTH2_CLIENT_ID", | 155 | if (!token) { |
| 84 | "GROK_OAUTH2_PRINCIPAL_TYPE", | 156 | return { email, usageLimits: makeUnavailableUsageLimits({ checkedAt, reason: "unsupported" }) }; |
| 85 | "GROK_OAUTH2_PRINCIPAL_ID", | 157 | } |
| 86 | "GROK_AUTH_PROVIDER_COMMAND", | 158 | // A failed quota request still knows which account it asked about. |
| 87 | "GROK_LOCAL_AUTH", | 159 | const usageLimits = yield* Effect.gen(function* () { |
| 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" }); | ||
| 134 | const client = yield* HttpClient.HttpClient; | 160 | const client = yield* HttpClient.HttpClient; |
| 135 | const response = yield* client.execute( | 161 | const response = yield* client.execute( |
| 136 | HttpClientRequest.get("https://cli-chat-proxy.grok.com/v1/billing?format=credits").pipe( | 162 | HttpClientRequest.get("https://cli-chat-proxy.grok.com/v1/billing?format=credits").pipe( |
| ... | @@ -143,14 +169,7 @@ export const readGrokUsageLimits = Effect.fn("readGrokUsageLimits")(function* ( | ... | @@ -143,14 +169,7 @@ export const readGrokUsageLimits = Effect.fn("readGrokUsageLimits")(function* ( |
| 143 | return grokUsageResponseToLimits(body, checkedAt); | 169 | return grokUsageResponseToLimits(body, checkedAt); |
| 144 | }).pipe( | 170 | }).pipe( |
| 145 | Effect.timeout("10 seconds"), | 171 | Effect.timeout("10 seconds"), |
| 146 | Effect.catch(() => | 172 | Effect.catch(() => Effect.succeed(probeFailed)), |
| 147 | Effect.succeed( | ||
| 148 | makeUnavailableUsageLimits({ | ||
| 149 | checkedAt, | ||
| 150 | reason: "probeFailed", | ||
| 151 | message: "Grok could not read usage limits.", | ||
| 152 | }), | ||
| 153 | ), | ||
| 154 | ), | ||
| 155 | ); | 173 | ); |
| 174 | return { email, usageLimits }; | ||
| 156 | }); | 175 | }); |