feat: add Grok authentication plugin with OAuth support

- Introduced a new Grok authentication plugin, including essential files such as main.ts, provider.ts, and resources.ts.
- Implemented OAuth2 device authorization flow in oauth.ts, allowing users to sign in with xAI.
- Added model discovery and quota management functionalities in models.ts and resources.ts.
- Created a JSON configuration file (plugin.json) for plugin metadata and permissions.
- Included SVG asset for the Grok icon.
- Developed comprehensive tests in grok_test.ts to ensure functionality and reliability of the plugin.
This commit is contained in:
leookun
2026-08-30 22:19:21 +08:00
parent 3184615719
commit 21048fb34b
13 changed files with 1525 additions and 4 deletions
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="230" height="230" viewBox="0 0 230 230" xmlns="http://www.w3.org/2000/svg">
<title>grok</title>
<rect x="22" y="21" width="187" height="187" rx="42" fill="#000000"/>
<g fill="#FFFFFF">
<path d="M96.5 137.5 L152 82 L166 96 L110.5 151.5 Z"/>
<path d="M64 82 L106 124 L92 138 L64 110 Z"/>
<path d="M166 148 L166 96 L152 110 L152 148 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 418 B

@@ -0,0 +1,13 @@
{
"imports": {
"cursor-byok:plugin": "../../../src/plugin/sdk/plugin.ts",
"cursor-byok:provider": "../../../src/plugin/sdk/provider.ts",
"cursor-byok:model": "../../../src/plugin/sdk/model.ts",
"cursor-byok:resource": "../../../src/plugin/sdk/resource.ts",
"cursor-byok:protocol/openai-chat": "../../../src/plugin/sdk/protocol/openai_chat.ts"
},
"fmt": {
"lineWidth": 100,
"exclude": ["assets"]
}
}
@@ -0,0 +1,380 @@
import type {
JsonValue,
NetworkEventStream,
NetworkResponse,
PluginContext,
} from "cursor-byok:plugin";
import type { LlmRequest, ModelEvent } from "cursor-byok:provider";
import type { ResourceSnapshot } from "cursor-byok:resource";
import { grokDeviceOAuth } from "./oauth.ts";
import { FALLBACK_MODELS, grokModels, parseGrokModels } from "./models.ts";
import { grokProvider, isQuotaError } from "./provider.ts";
import {
accountIdentity,
credentialDraft,
parseCredentialFiles,
parseGrokUsage,
presentAccount,
quotaState,
RESOURCE_TYPE,
} from "./resources.ts";
function assert(condition: unknown, message = "assertion failed"): asserts condition {
if (!condition) throw new Error(message);
}
function assertEquals(actual: unknown, expected: unknown): void {
const left = JSON.stringify(actual);
const right = JSON.stringify(expected);
if (left !== right) throw new Error(`expected ${right}, received ${left}`);
}
function jwt(payload: Record<string, unknown>): string {
const encoded = btoa(JSON.stringify(payload)).replace(/=/g, "").replace(/\+/g, "-").replace(
/\//g,
"_",
);
return `header.${encoded}.signature`;
}
type RequestInit = { body?: string; headers?: Record<string, string> };
type FetchHandler = (url: string, init?: RequestInit) => NetworkResponse;
type StreamHandler = (url: string, init?: RequestInit) => NetworkEventStream;
function context(handlers: { fetch?: FetchHandler; stream?: StreamHandler }): PluginContext {
return {
network: {
fetch: (url, init) => {
if (!handlers.fetch) throw new Error("fetch was not expected");
return Promise.resolve(handlers.fetch(url, init));
},
stream: (url, init) => {
if (!handlers.stream) throw new Error("stream was not expected");
return Promise.resolve(handlers.stream(url, init));
},
},
signal: new AbortController().signal,
};
}
function snapshot(privateData: JsonValue): ResourceSnapshot {
return {
id: "resource-1",
type: RESOURCE_TYPE,
key: "grok:user-1",
privateData,
state: { status: "ready" },
};
}
async function* sse(lines: string[]): AsyncGenerator<string> {
for (const line of lines) yield line;
}
function request(): LlmRequest {
return {
instructions: "You are a coding assistant.",
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
tools: [],
reasoning: { enabled: true, effort: "medium" },
latency: "fast",
maxOutputTokens: 32_000,
cacheKey: "conversation-1",
};
}
Deno.test("account identity uses the JWT subject and drafts keep tokens private-side", async () => {
const token = jwt({ sub: "user-1", email: "person@x.ai" });
assertEquals(await accountIdentity(token), {
key: "grok:user-1",
displayName: "person@x.ai",
});
const draft = await credentialDraft({
accessToken: token,
refreshToken: null,
displayName: null,
});
assertEquals(draft.key, "grok:user-1");
const view = presentAccount(snapshot(draft.privateData));
assert(!JSON.stringify(view).includes(token), "resource view exposed an access token");
assertEquals(view.displayName, "person@x.ai");
});
Deno.test("credential import accepts Grok credential JSON files", () => {
const { credentials, warnings } = parseCredentialFiles([
{
name: "accounts.json",
content: JSON.stringify({
accounts: [
{ access_token: "token-1", refresh_token: "refresh-1", email: "a@x.ai" },
{ access_token: "token-2", disabled: true },
],
}),
},
{ name: "broken.json", content: "{not json" },
]);
assertEquals(credentials, [{
accessToken: "token-1",
refreshToken: "refresh-1",
displayName: "a@x.ai",
}]);
assertEquals(warnings, ["broken.json: not valid JSON"]);
});
Deno.test("credit usage percent is inverted to remaining and drives cooling", () => {
const quota = parseGrokUsage({
config: {
creditUsagePercent: 34,
subscriptionTierDisplay: "SuperGrok",
currentPeriod: { end: "2026-09-01T00:00:00Z" },
},
}, 1_700_000_000_000);
assertEquals(quota.planLabel, "SuperGrok");
assertEquals(quota.remainingPercent, 66);
assertEquals(quota.resetAtMs, Date.parse("2026-09-01T00:00:00Z"));
assertEquals(quotaState(quota, 1_700_000_000_000), { status: "ready" });
const exhausted = parseGrokUsage({
config: { creditUsagePercent: 100, currentPeriod: { end: 1_900_000_000 } },
}, 1_700_000_000_000);
assertEquals(quotaState(exhausted, 1_700_000_000_000), {
status: "cooling",
retryAtMs: 1_900_000_000_000,
message: "Grok credits are exhausted",
});
});
Deno.test("missing usage with a billing period counts as unused", () => {
const quota = parseGrokUsage({ config: { currentPeriod: { end: 1_900_000_000 } } });
assertEquals(quota.remainingPercent, 100);
assertEquals(quota.limitReached, false);
});
Deno.test("model discovery parses both language-models and standard list shapes", () => {
const richModels = parseGrokModels({
models: [
{ id: "grok-4", input_modalities: ["text", "image"], context_window: 256_000 },
{ id: "grok-3-mini", input_modalities: ["text"] },
{ id: "grok-4" },
],
});
assertEquals(richModels.map((model) => model.id), ["grok-4", "grok-3-mini"]);
assertEquals(richModels[0].displayName, "Grok 4");
assertEquals(richModels[0].contextWindowTokens, 256_000);
assertEquals(richModels[0].capabilities, { thinking: false, images: true });
assertEquals(richModels[1].capabilities, { thinking: false, images: false });
const plainModels = parseGrokModels({ data: [{ id: "grok-4-fast" }] });
assertEquals(plainModels.map((model) => model.id), ["grok-4-fast"]);
assertEquals(plainModels[0].displayName, "Grok 4 Fast");
});
Deno.test("model discovery falls back to known models when the account cannot list", async () => {
const token = jwt({ sub: "user-1" });
const draft = await credentialDraft({
accessToken: token,
refreshToken: null,
displayName: null,
});
const models = await grokModels.list(
{ resource: snapshot(draft.privateData) },
context({
fetch: () => ({
status: 403,
headers: {},
body: JSON.stringify({ code: "personal-team-blocked:spending-limit" }),
}),
}),
);
assertEquals(models, FALLBACK_MODELS);
});
Deno.test("device OAuth begins with a host-held session and completes with a resource draft", async () => {
const accessToken = jwt({ sub: "user-oauth", email: "oauth@x.ai" });
let requestNumber = 0;
const flowContext = context({
fetch: (url, init) => {
requestNumber += 1;
if (requestNumber === 1) {
assertEquals(url, "https://auth.x.ai/oauth2/device/code");
assert(init?.body?.includes("scope="), "device code request must carry the scope");
return {
status: 200,
headers: {},
body: JSON.stringify({
device_code: "private-device-code",
user_code: "ABCD-EFGH",
verification_uri: "https://accounts.x.ai/activate",
verification_uri_complete: "https://accounts.x.ai/activate?code=ABCD-EFGH",
expires_in: 900,
interval: 5,
}),
};
}
assertEquals(url, "https://auth.x.ai/oauth2/token");
assert(init?.body?.includes("device_code=private-device-code"));
if (requestNumber === 2) {
return {
status: 400,
headers: {},
body: JSON.stringify({ error: "authorization_pending" }),
};
}
return {
status: 200,
headers: {},
body: JSON.stringify({ access_token: accessToken, refresh_token: "refresh-secret" }),
};
},
});
const begun = await grokDeviceOAuth.begin(flowContext);
assertEquals(begun.userCode, "ABCD-EFGH");
assertEquals(begun.pollIntervalMs, 5000);
const pending = await grokDeviceOAuth.poll(begun.session, flowContext);
assertEquals(pending.status, "pending");
const polled = await grokDeviceOAuth.poll(begun.session, flowContext);
assert(polled.status === "completed", `expected completed, received ${polled.status}`);
assertEquals(polled.resources[0].key, "grok:user-oauth");
assertEquals(requestNumber, 3);
});
Deno.test("invoke streams normalized events from the xAI Chat Completions API", async () => {
const token = jwt({ sub: "user-1" });
const draft = await credentialDraft({
accessToken: token,
refreshToken: null,
displayName: null,
});
let requestBody = "";
let requestHeaders: Record<string, string> = {};
const events: ModelEvent[] = [];
const result = await grokProvider.invoke(
{
model: { id: "grok-4", displayName: "Grok 4" },
resource: snapshot(draft.privateData),
request: request(),
},
{ emit: (event) => events.push(event) },
context({
stream: (url, init) => {
assertEquals(url, "https://api.x.ai/v1/chat/completions");
requestBody = init?.body ?? "";
requestHeaders = init?.headers ?? {};
return {
status: 200,
headers: {},
lines: sse([
'data: {"choices":[{"delta":{"content":"Hel"}}]}',
'data: {"choices":[{"delta":{"content":"lo"}}]}',
'data: {"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":10,"completion_tokens":2,"prompt_tokens_details":{"cached_tokens":4}}}',
"data: [DONE]",
]),
};
},
}),
);
assertEquals(result, { status: "completed" });
const body = JSON.parse(requestBody) as Record<string, unknown>;
assertEquals(body.model, "grok-4");
assertEquals(body.stream, true);
assertEquals(body.prompt_cache_key, "conversation-1");
assert(!("reasoning_effort" in body), "xAI endpoint rejects reasoning_effort");
assert(!("service_tier" in body), "xAI endpoint rejects service_tier");
assertEquals(requestHeaders["authorization"], `Bearer ${token}`);
assertEquals(events, [
{ type: "text-start" },
{ type: "text-delta", text: "Hel" },
{ type: "text-delta", text: "lo" },
{ type: "text-end" },
{
type: "usage",
usage: {
inputTokens: 10,
outputTokens: 2,
totalTokens: null,
cacheReadTokens: 4,
cacheWriteTokens: null,
reasoningTokens: null,
},
},
{ type: "done", reason: "stop" },
]);
});
Deno.test("invoke streams incremental tool calls and reasoning replay state", async () => {
const token = jwt({ sub: "user-1" });
const draft = await credentialDraft({
accessToken: token,
refreshToken: null,
displayName: null,
});
const events: ModelEvent[] = [];
const result = await grokProvider.invoke(
{
model: { id: "grok-4", displayName: "Grok 4" },
resource: snapshot(draft.privateData),
request: request(),
},
{ emit: (event) => events.push(event) },
context({
stream: () => ({
status: 200,
headers: {},
lines: sse([
'data: {"choices":[{"delta":{"reasoning_content":"thinking"}}]}',
'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call-1","function":{"name":"read_file","arguments":"{\\"path\\":"}}]}}]}',
'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\\"a.ts\\"}"}}]}}]}',
'data: {"choices":[{"delta":{},"finish_reason":"tool_calls"}]}',
"data: [DONE]",
]),
}),
}),
);
assertEquals(result, { status: "completed" });
assertEquals(events, [
{ type: "thinking-start" },
{ type: "thinking-delta", text: "thinking" },
{ type: "tool-call-start", index: 0, callId: "call-1", name: "read_file" },
{ type: "tool-call-arguments-delta", index: 0, delta: '{"path":' },
{ type: "tool-call-arguments-delta", index: 0, delta: '"a.ts"}' },
{ type: "thinking-end" },
{ type: "tool-call-end", index: 0 },
{
type: "replay-state",
providerKind: "openai_chat",
value: { reasoning_content: "thinking" },
},
{ type: "done", reason: "tool-use" },
]);
});
Deno.test("invoke maps quota failures to a cooling resource error", async () => {
assert(!isQuotaError("400 invalid request"));
assert(isQuotaError("429 credits exhausted"));
const token = jwt({ sub: "user-1" });
const draft = await credentialDraft({
accessToken: token,
refreshToken: null,
displayName: null,
});
const result = await grokProvider.invoke(
{
model: { id: "grok-4", displayName: "Grok 4" },
resource: snapshot(draft.privateData),
request: request(),
},
{ emit: () => {} },
context({
stream: () => ({
status: 429,
headers: {},
lines: sse(['{"error":"credits exhausted"}']),
}),
}),
);
assert(result.status === "resource-error", `expected resource-error, received ${result.status}`);
assert(result.patch.state?.status === "cooling", "quota failure should cool the resource");
});
+16
View File
@@ -0,0 +1,16 @@
import { defineProviderPlugin } from "cursor-byok:plugin";
import { grokDeviceOAuth } from "./oauth.ts";
import { grokProvider } from "./provider.ts";
import { credentialImport, presentAccount, refreshAccount, RESOURCE_TYPE } from "./resources.ts";
export default defineProviderPlugin({
providers: [grokProvider],
resources: [{
type: RESOURCE_TYPE,
displayName: { "en-US": "Grok accounts", "zh-CN": "Grok 账号" },
add: [grokDeviceOAuth],
import: credentialImport,
present: presentAccount,
refresh: refreshAccount,
}],
});
+110
View File
@@ -0,0 +1,110 @@
import type { ModelDefinition, ModelSupport } from "cursor-byok:model";
import { accountData } from "./resources.ts";
const LANGUAGE_MODELS_URL = "https://api.x.ai/v1/language-models";
const MODELS_URL = "https://api.x.ai/v1/models";
/** 免费账号无权调用模型列表接口(403 spending-limit);退回已知模型。 */
export const FALLBACK_MODELS: ModelDefinition[] = [
{
id: "grok-4.6",
displayName: "Grok 4.6",
capabilities: { thinking: false, images: true },
},
{
id: "grok-4.5",
displayName: "Grok 4.5",
capabilities: { thinking: false, images: true },
},
];
function object(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: null;
}
function text(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function positiveInteger(value: unknown): number | null {
const parsed = typeof value === "number"
? value
: typeof value === "string"
? Number(value)
: NaN;
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : null;
}
function modalities(value: unknown): string[] {
return Array.isArray(value)
? value.flatMap((item) => (typeof item === "string" ? [item.toLowerCase()] : []))
: [];
}
/** 把模型 ID 变成可读名称,如 grok-4-fast → Grok 4 Fast。 */
function displayName(id: string): string {
return id
.split("-")
.map((part) => (/^\d/.test(part) ? part : part.charAt(0).toUpperCase() + part.slice(1)))
.join(" ");
}
/** 兼容 /v1/language-models 的 models 数组与 /v1/models 的 data 数组。 */
export function parseGrokModels(body: unknown): ModelDefinition[] {
const root = object(body);
const source = root?.models ?? root?.data ?? body;
if (!Array.isArray(source)) {
throw new Error("Grok model discovery response does not contain a model list");
}
const seen = new Set<string>();
const models: ModelDefinition[] = [];
for (const raw of source) {
const model = object(raw);
const id = model ? text(model.id ?? model.name) : null;
if (!id || seen.has(id)) continue;
seen.add(id);
const inputs = modalities(model?.input_modalities ?? model?.inputModalities);
const contextWindowTokens = positiveInteger(
model?.context_window ?? model?.contextWindow ?? model?.max_prompt_length,
);
models.push({
id,
displayName: displayName(id),
...(contextWindowTokens !== null ? { contextWindowTokens } : {}),
capabilities: {
thinking: false,
images: inputs.length === 0 || inputs.includes("image"),
},
});
}
return models;
}
export const grokModels: ModelSupport = {
list: async ({ resource }, context): Promise<ModelDefinition[]> => {
if (!resource) throw new Error("add a Grok account before syncing models");
const data = accountData(resource);
const headers = {
accept: "application/json",
authorization: `Bearer ${data.accessToken}`,
};
// language-models 带模态与上下文元数据;不可用时回退到标准列表。
let response = await context.network.fetch(LANGUAGE_MODELS_URL, { method: "GET", headers });
if (response.status < 200 || response.status >= 300) {
response = await context.network.fetch(MODELS_URL, { method: "GET", headers });
}
if (response.status < 200 || response.status >= 300) {
return FALLBACK_MODELS;
}
let body: unknown;
try {
body = JSON.parse(response.body);
} catch {
throw new Error("Grok model discovery returned invalid JSON");
}
const models = parseGrokModels(body);
return models.length > 0 ? models : FALLBACK_MODELS;
},
};
+148
View File
@@ -0,0 +1,148 @@
import type { JsonValue, PluginContext } from "cursor-byok:plugin";
import type { OAuth2AddMethod, OAuth2Begin, OAuth2Poll } from "cursor-byok:resource";
import { credentialDraft } from "./resources.ts";
const CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
const DEVICE_CODE_URL = "https://auth.x.ai/oauth2/device/code";
const TOKEN_URL = "https://auth.x.ai/oauth2/token";
const SCOPE = "openid profile email offline_access grok-cli:access api:access";
type Session = {
deviceCode: string;
};
function object(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: null;
}
function text(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function number(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim()) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
function parseBody(body: string): Record<string, unknown> {
try {
return object(JSON.parse(body)) ?? {};
} catch {
return {};
}
}
function parseSession(value: JsonValue): Session {
const session = object(value);
const deviceCode = text(session?.deviceCode);
if (!deviceCode) throw new Error("Grok OAuth session is invalid");
return { deviceCode };
}
async function begin(context: PluginContext): Promise<OAuth2Begin> {
const response = await context.network.fetch(DEVICE_CODE_URL, {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({ client_id: CLIENT_ID, scope: SCOPE }).toString(),
});
const body = parseBody(response.body);
if (response.status < 200 || response.status >= 300) {
throw new Error(
`Failed to request xAI device code (HTTP ${response.status}): ${response.body}`,
);
}
const deviceCode = text(body.device_code);
const userCode = text(body.user_code);
const verificationUrl = text(body.verification_uri);
if (!deviceCode || !userCode || !verificationUrl) {
throw new Error("xAI device authorization response is incomplete");
}
const session: Session = { deviceCode };
return {
session: session as unknown as JsonValue,
userCode,
verificationUrl,
...(text(body.verification_uri_complete)
? { verificationUrlComplete: text(body.verification_uri_complete)! }
: {}),
expiresAtMs: Date.now() + Math.max(1, number(body.expires_in) ?? 900) * 1000,
pollIntervalMs: Math.max(1, number(body.interval) ?? 5) * 1000,
};
}
async function poll(sessionValue: JsonValue, context: PluginContext): Promise<OAuth2Poll> {
const session = parseSession(sessionValue);
const response = await context.network.fetch(TOKEN_URL, {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
client_id: CLIENT_ID,
device_code: session.deviceCode,
}).toString(),
});
const body = parseBody(response.body);
if (response.status >= 200 && response.status < 300) {
const accessToken = text(body.access_token);
if (!accessToken) {
return { status: "failed", message: "xAI token response is missing access_token" };
}
return {
status: "completed",
resources: [
await credentialDraft({
accessToken,
refreshToken: text(body.refresh_token),
displayName: null,
}),
],
};
}
const code = text(body.error) ?? "";
const message = text(body.error_description);
switch (code) {
case "authorization_pending":
return { status: "pending" };
case "slow_down":
return { status: "slow-down" };
case "expired_token":
return { status: "failed", message: message ?? "Device authorization code expired" };
case "access_denied":
return { status: "denied", ...(message ? { message } : {}) };
default:
return {
status: "failed",
message: message ??
(code
? `OAuth error: ${code}`
: `xAI device authorization failed (HTTP ${response.status})`),
};
}
}
export const grokDeviceOAuth: OAuth2AddMethod = {
type: "oauth2.0",
id: "xai-device",
displayName: {
"en-US": "Sign in with xAI",
"zh-CN": "使用 xAI 登录",
},
description: {
"en-US": "Authorize this device with xAI, then add the resulting Grok account.",
"zh-CN": "在 xAI 完成设备授权后,自动添加对应的 Grok 账号。",
},
begin,
poll,
};
@@ -0,0 +1,17 @@
{
"apiVersion": 1,
"id": "dev.cursorbyok.examples.grok-auth",
"name": "Grok",
"version": "0.1.0",
"author": "@leookun",
"minAppVersion": "0.1.0",
"icon": "assets/grok.svg",
"entry": "main.ts",
"permissions": {
"network": [
"auth.x.ai",
"api.x.ai",
"cli-chat-proxy.grok.com"
]
}
}
@@ -0,0 +1,112 @@
import type {
ProviderInvokeInput,
ProviderOutput,
ProviderResult,
ProviderSupport,
} from "cursor-byok:provider";
import type { PluginContext } from "cursor-byok:plugin";
import { HttpError, streamOpenAiChat } from "cursor-byok:protocol/openai-chat";
import { grokModels } from "./models.ts";
import { type AccountData, accountData, quotaExhaustedPatch, RESOURCE_TYPE } from "./resources.ts";
const CHAT_URL = "https://api.x.ai/v1/chat/completions";
/** 流内错误只有文本可用,按积分/额度关键词分类。 */
export function isQuotaError(error: string): boolean {
const message = error.toLowerCase();
return message.includes("insufficient_quota") ||
message.includes("credits exhausted") ||
message.includes("out of credits") ||
message.includes("quota_exceeded") ||
(message.includes("429") &&
(message.includes("quota") || message.includes("credit") ||
message.includes("insufficient")));
}
/** HTTP 失败携带结构化状态码,429 一律按额度耗尽处理并冷却账号。 */
function isQuotaHttpError(error: HttpError): boolean {
if (error.status === 429) return true;
const body = error.body.toLowerCase();
return body.includes("insufficient_quota") ||
body.includes("credits exhausted") ||
body.includes("out of credits") ||
// 免费账号触达消费上限时返回 403 spending-limit,属于额度而非授权问题。
body.includes("spending-limit") ||
body.includes("run out of credits") ||
body.includes("quota_exceeded");
}
function invalidResult(message: string, stateMessage: string): ProviderResult {
return {
status: "resource-error",
message,
patch: { state: { status: "invalid", message: stateMessage } },
};
}
async function invoke(
input: ProviderInvokeInput,
output: ProviderOutput,
context: PluginContext,
): Promise<ProviderResult> {
if (!input.resource) {
return { status: "request-error", message: "add a Grok account before calling Grok" };
}
let data: AccountData;
try {
data = accountData(input.resource);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return invalidResult(message, message);
}
try {
await streamOpenAiChat(
{
url: CHAT_URL,
model: input.model.id,
// xAI 不接受 reasoning_effort 与 service_tier;思考由模型自身决定。
request: {
...input.request,
reasoning: { enabled: false, effort: null },
latency: "standard",
},
headers: { authorization: `Bearer ${data.accessToken}` },
},
output,
context,
);
return { status: "completed" };
} catch (error) {
if (error instanceof HttpError) {
if ((error.status === 401 || error.status === 403) && !isQuotaHttpError(error)) {
return invalidResult(error.message, "Grok authorization expired; sign in again");
}
if (isQuotaHttpError(error)) {
return {
status: "resource-error",
message: error.message,
patch: quotaExhaustedPatch(data),
};
}
return { status: "request-error", message: error.message };
}
const message = error instanceof Error ? error.message : String(error);
if (isQuotaError(message)) {
return { status: "resource-error", message, patch: quotaExhaustedPatch(data) };
}
return { status: "request-error", message };
}
}
export const grokProvider: ProviderSupport = {
id: "grok",
displayName: "xAI Grok",
description: {
"en-US": "SuperGrok subscription access through the official Grok CLI endpoint.",
"zh-CN": "通过官方 Grok CLI 接口使用 SuperGrok 订阅。",
},
providerType: "xai",
resourceType: RESOURCE_TYPE,
models: grokModels,
invoke,
};
@@ -0,0 +1,342 @@
import type { JsonValue, PluginContext } from "cursor-byok:plugin";
import type {
ResourceDraft,
ResourceImportFile,
ResourceImportResult,
ResourceImportSupport,
ResourceMetric,
ResourcePatch,
ResourceSnapshot,
ResourceState,
ResourceView,
} from "cursor-byok:resource";
export const RESOURCE_TYPE = "grok-account";
const CREDITS_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
const ONE_HOUR_MS = 60 * 60 * 1000;
export type AccountQuota = {
planLabel: string | null;
usedPercent: number | null;
remainingPercent: number | null;
resetAtMs: number | null;
limitReached: boolean;
updatedAtMs: number;
};
/** 单条 grok-account 资源的 privateData 形状。 */
export type AccountData = {
accessToken: string;
refreshToken: string | null;
displayName: string;
quota: AccountQuota | null;
};
export type CredentialCandidate = {
accessToken: string;
refreshToken: string | null;
displayName: string | null;
};
function object(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: null;
}
function text(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function number(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim()) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
function decodeJwtPayload(token: string): Record<string, unknown> | null {
const encoded = token.split(".")[1];
if (!encoded) return null;
try {
const normalized = encoded.replace(/-/g, "+").replace(/_/g, "/");
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
const bytes = Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));
return object(JSON.parse(new TextDecoder().decode(bytes)));
} catch {
return null;
}
}
function claim(payload: Record<string, unknown> | null, key: string): string | null {
return payload ? text(payload[key]) : null;
}
async function tokenFingerprint(token: string): Promise<string> {
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token));
return Array.from(
new Uint8Array(digest).slice(0, 8),
(byte) => byte.toString(16).padStart(2, "0"),
).join("");
}
export async function accountIdentity(
accessToken: string,
): Promise<{ key: string; displayName: string }> {
const payload = decodeJwtPayload(accessToken);
const identity = claim(payload, "sub") ??
claim(payload, "email") ??
await tokenFingerprint(accessToken);
const displayName = claim(payload, "email") ??
claim(payload, "preferred_username") ??
claim(payload, "name") ??
identity;
return { key: `grok:${identity}`, displayName };
}
export async function credentialDraft(credential: CredentialCandidate): Promise<ResourceDraft> {
const identity = await accountIdentity(credential.accessToken);
const data: AccountData = {
accessToken: credential.accessToken,
refreshToken: credential.refreshToken,
displayName: credential.displayName ?? identity.displayName,
quota: null,
};
return { key: identity.key, privateData: data as unknown as JsonValue };
}
export function accountData(resource: ResourceSnapshot): AccountData {
const data = object(resource.privateData);
const accessToken = text(data?.accessToken);
if (!accessToken) throw new Error("Grok account resource is missing its access token");
return {
accessToken,
refreshToken: text(data?.refreshToken),
displayName: text(data?.displayName) ?? "Grok account",
quota: (data?.quota ?? null) as AccountQuota | null,
};
}
function clampPercent(value: number): number {
return Math.max(0, Math.min(100, value));
}
function resetAtMs(value: unknown): number | null {
const numeric = number(value);
if (numeric !== null) return numeric > 10_000_000_000 ? numeric : numeric * 1000;
if (typeof value === "string") {
const parsed = Date.parse(value);
if (Number.isFinite(parsed)) return parsed;
}
return null;
}
/** 解析 Grok CLI 计费接口的积分响应;creditUsagePercent 表示已用占比。 */
export function parseGrokUsage(body: unknown, nowMs = Date.now()): AccountQuota {
const root = object(body) ?? {};
const config = object(root.config) ?? root;
let used = number(config.creditUsagePercent ?? config.credit_usage_percent);
if (used === null) {
const onDemandUsed = number(config.onDemandUsed ?? config.on_demand_used);
const onDemandCap = number(config.onDemandCap ?? config.on_demand_cap);
if (onDemandUsed !== null && onDemandCap !== null && onDemandCap > 0) {
used = (onDemandUsed / onDemandCap) * 100;
}
}
// 存在计费周期但没有用量字段时视为未使用。
if (used === null && (config.currentPeriod ?? config.current_period) !== undefined) {
used = 0;
}
const remaining = used === null ? null : clampPercent(100 - used);
const period = object(config.currentPeriod ?? config.current_period);
return {
planLabel: text(
config.subscriptionTierDisplay ?? config.subscription_tier_display ??
config.subscriptionTier ?? config.product,
),
usedPercent: used === null ? null : clampPercent(used),
remainingPercent: remaining,
resetAtMs: resetAtMs(period?.end ?? config.billingPeriodEnd ?? config.billing_period_end),
limitReached: remaining !== null && remaining <= 0,
updatedAtMs: nowMs,
};
}
export function quotaState(quota: AccountQuota | null, nowMs = Date.now()): ResourceState {
if (!quota || !quota.limitReached) return { status: "ready" };
if (quota.resetAtMs !== null && quota.resetAtMs <= nowMs) return { status: "ready" };
return {
status: "cooling",
retryAtMs: quota.resetAtMs ?? nowMs + ONE_HOUR_MS,
message: "Grok credits are exhausted",
};
}
/** 额度耗尽时的资源补丁:标记积分耗尽并进入冷却,重置时间未知时回退 1 小时。 */
export function quotaExhaustedPatch(data: AccountData, nowMs = Date.now()): ResourcePatch {
const quota: AccountQuota = {
planLabel: data.quota?.planLabel ?? null,
usedPercent: 100,
remainingPercent: 0,
resetAtMs: data.quota?.resetAtMs !== undefined && data.quota?.resetAtMs !== null &&
data.quota.resetAtMs > nowMs
? data.quota.resetAtMs
: null,
limitReached: true,
updatedAtMs: nowMs,
};
return {
privateData: { ...data, quota } as unknown as JsonValue,
state: quotaState(quota, nowMs),
};
}
export function accountHeaders(data: AccountData): Record<string, string> {
return {
accept: "application/json",
authorization: `Bearer ${data.accessToken}`,
// Grok CLI 计费接口要求该头标识客户端来源。
"x-xai-token-auth": "xai-grok-cli",
};
}
function jwtDisplayName(token: string | null): string | null {
if (!token) return null;
const payload = decodeJwtPayload(token);
return claim(payload, "email") ?? claim(payload, "preferred_username") ??
claim(payload, "name");
}
export function presentAccount(resource: ResourceSnapshot): ResourceView {
const data = accountData(resource);
const metrics: ResourceMetric[] = [];
const quota = data.quota;
if (quota && quota.remainingPercent !== null) {
metrics.push({
id: "credits",
label: { "en-US": "Credits", "zh-CN": "积分额度" },
unit: "percent",
value: quota.remainingPercent,
...(quota.resetAtMs !== null ? { resetAtMs: quota.resetAtMs } : {}),
});
}
return {
// 旧记录可能存的是账号 ID;展示时优先从 token 现算邮箱。
displayName: jwtDisplayName(data.accessToken) ?? data.displayName,
...(quota?.planLabel ? { description: quota.planLabel } : {}),
...(metrics.length > 0 ? { metrics } : {}),
};
}
export async function refreshAccount(
resource: ResourceSnapshot,
context: PluginContext,
): Promise<ResourcePatch> {
const data = accountData(resource);
const response = await context.network.fetch(CREDITS_URL, {
method: "GET",
headers: accountHeaders(data),
});
if (response.status < 200 || response.status >= 300) {
if (response.status === 401 || response.status === 403) {
return {
state: { status: "invalid", message: "Grok authorization expired; sign in again" },
};
}
throw new Error(`Grok usage lookup failed (HTTP ${response.status}): ${response.body}`);
}
let body: unknown;
try {
body = JSON.parse(response.body);
} catch {
throw new Error("Grok usage lookup returned invalid JSON");
}
const quota = parseGrokUsage(body);
return {
privateData: { ...data, quota } as unknown as JsonValue,
state: quotaState(quota),
};
}
function firstText(source: Record<string, unknown>, keys: string[]): string | null {
for (const key of keys) {
const value = text(source[key]);
if (value) return value;
}
return null;
}
function collectCredentials(value: unknown, output: CredentialCandidate[]): void {
if (Array.isArray(value)) {
for (const item of value) collectCredentials(item, output);
return;
}
const item = object(value);
if (!item || item.disabled === true) return;
for (const key of ["accounts", "credentials", "items"]) {
if (Array.isArray(item[key])) {
collectCredentials(item[key], output);
return;
}
}
const tokens = object(item.tokens) ?? item;
const accessToken = firstText(tokens, ["access_token", "accessToken", "token", "key"]) ??
firstText(item, ["access_token", "accessToken", "token", "key", "XAI_API_KEY"]);
if (!accessToken) return;
const refreshToken = firstText(tokens, ["refresh_token", "refreshToken"]) ??
firstText(item, ["refresh_token", "refreshToken"]);
const displayName = firstText(item, ["email", "display_name", "displayName", "name"]) ??
firstText(tokens, ["email", "display_name", "displayName", "name"]);
output.push({ accessToken, refreshToken, displayName });
}
export function parseCredentialFiles(files: ResourceImportFile[]): {
credentials: CredentialCandidate[];
warnings: string[];
} {
const credentials: CredentialCandidate[] = [];
const warnings: string[] = [];
for (const file of files) {
let content: unknown;
try {
content = JSON.parse(file.content);
} catch {
warnings.push(`${file.name}: not valid JSON`);
continue;
}
const found: CredentialCandidate[] = [];
collectCredentials(content, found);
if (found.length === 0) {
warnings.push(`${file.name}: no Grok access token found`);
continue;
}
credentials.push(...found);
}
return { credentials, warnings };
}
export const credentialImport: ResourceImportSupport = {
displayName: {
"en-US": "Import Grok credentials",
"zh-CN": "导入 Grok 凭证",
},
description: {
"en-US": "Import one or more Grok JSON credential files.",
"zh-CN": "导入一个或多个 Grok JSON 凭证文件。",
},
accept: [".json"],
multiple: true,
parse: async (files: ResourceImportFile[]): Promise<ResourceImportResult> => {
const { credentials, warnings } = parseCredentialFiles(files);
if (credentials.length === 0) {
throw new Error(warnings.join("; ") || "credential JSON does not contain an access token");
}
return {
resources: await Promise.all(credentials.map(credentialDraft)),
...(warnings.length > 0 ? { warnings } : {}),
};
},
};