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 } : {}),
};
},
};
+53 -1
View File
@@ -57,7 +57,59 @@ const CODEX_AUTH: &[(&str, &str)] = &[
),
];
const PLUGINS: &[(&str, &[(&str, &str)])] = &[("codex-auth", CODEX_AUTH)];
const GROK_AUTH: &[(&str, &str)] = &[
(
"plugin.json",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/plugins/build-in/grok-auth/plugin.json"
)),
),
(
"main.ts",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/plugins/build-in/grok-auth/main.ts"
)),
),
(
"provider.ts",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/plugins/build-in/grok-auth/provider.ts"
)),
),
(
"models.ts",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/plugins/build-in/grok-auth/models.ts"
)),
),
(
"oauth.ts",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/plugins/build-in/grok-auth/oauth.ts"
)),
),
(
"resources.ts",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/plugins/build-in/grok-auth/resources.ts"
)),
),
(
"assets/grok.svg",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/plugins/build-in/grok-auth/assets/grok.svg"
)),
),
];
const PLUGINS: &[(&str, &[(&str, &str)])] = &[("codex-auth", CODEX_AUTH), ("grok-auth", GROK_AUTH)];
/// 把内置插件预装到 installed 目录。manifest 的 version 是缓存键:
/// 版本一致时零写盘;版本变化时整目录同步并清理旧版本残留文件。
+5 -2
View File
@@ -13,7 +13,6 @@ use crate::{config, Error, Result};
const DEFINITION_TIMEOUT: Duration = Duration::from_secs(10);
const MAX_OUTPUT_BYTES: u64 = 2 * 1024 * 1024;
const OUTPUT_PREFIX: &str = "CURSOR_BYOK_PLUGIN_DEFINITION:";
const IMPORT_MAP: &str = r#"{"imports":{"cursor-byok:plugin":"./plugin.ts","cursor-byok:provider":"./provider.ts","cursor-byok:model":"./model.ts","cursor-byok:resource":"./resource.ts","cursor-byok:protocol/openai-responses":"./protocol/openai_responses.ts"}}"#;
#[derive(Clone)]
pub struct PluginDefinitionLoader {
@@ -43,7 +42,7 @@ impl PluginDefinitionLoader {
let deno_dir = sdk_dir.join("cache");
std::fs::create_dir_all(&deno_dir)?;
let modules = [
(&import_map, IMPORT_MAP),
(&import_map, include_str!("sdk/import-map.json")),
(&collector, include_str!("sdk/collect.ts")),
(&worker, include_str!("sdk/worker.ts")),
(&sdk_dir.join("plugin.ts"), include_str!("sdk/plugin.ts")),
@@ -60,6 +59,10 @@ impl PluginDefinitionLoader {
&sdk_dir.join("protocol/openai_responses.ts"),
include_str!("sdk/protocol/openai_responses.ts"),
),
(
&sdk_dir.join("protocol/openai_chat.ts"),
include_str!("sdk/protocol/openai_chat.ts"),
),
];
for (path, content) in &modules {
write_if_changed(path, content)?;
+2 -1
View File
@@ -4,6 +4,7 @@
"cursor-byok:provider": "./provider.ts",
"cursor-byok:model": "./model.ts",
"cursor-byok:resource": "./resource.ts",
"cursor-byok:protocol/openai-responses": "./protocol/openai_responses.ts"
"cursor-byok:protocol/openai-responses": "./protocol/openai_responses.ts",
"cursor-byok:protocol/openai-chat": "./protocol/openai_chat.ts"
}
}
@@ -0,0 +1,317 @@
import type { JsonValue, PluginContext } from "../plugin.ts";
import type { LlmContentPart, LlmRequest, ModelEvent, ProviderOutput } from "../provider.ts";
/** 本协议产生的回放状态种类;与宿主内置 Chat Provider 一致,可互相回放。 */
export const REPLAY_KIND = "openai_chat";
/** 上游返回非 2xx 时抛出,携带完整响应体供调用方分类。 */
export class HttpError extends Error {
constructor(readonly status: number, readonly body: string) {
super(`HTTP ${status}: ${body}`);
}
}
export type OpenAiChatCall = {
url: string;
model: string;
request: LlmRequest;
headers?: Record<string, string>;
/** 最后合并进请求体。 */
extraBody?: Record<string, JsonValue>;
};
function record(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 : null;
}
function count(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
/** 纯文本消息保持字符串形式;混合图片时展开为分块数组。 */
function chatContent(parts: LlmContentPart[]): JsonValue {
if (parts.every((part) => part.type === "text")) {
return parts.map((part) => part.type === "text" ? part.text : "").join("");
}
return parts.map((part): JsonValue =>
part.type === "text" ? { type: "text", text: part.text } : {
type: "image_url",
image_url: { url: `data:${part.mediaType};base64,${part.dataBase64}` },
}
);
}
export function buildChatBody(call: OpenAiChatCall): Record<string, JsonValue> {
const messages: JsonValue[] = [];
if (call.request.instructions) {
messages.push({ role: "system", content: call.request.instructions });
}
for (const message of call.request.messages) {
if (message.role === "assistant") {
const reasoning = message.replayState?.providerKind === REPLAY_KIND
? text(record(message.replayState.value)?.reasoning_content)
: null;
// Chat Completions 拒绝空字符串的 assistant content;完全无可见
// 内容的 assistant 消息不需要发送。
if (!message.text && message.toolCalls.length === 0 && !reasoning) continue;
const value: Record<string, JsonValue> = {
role: "assistant",
content: message.text ? message.text : null,
};
if (reasoning) value.reasoning_content = reasoning;
if (message.toolCalls.length > 0) {
value.tool_calls = message.toolCalls.map((toolCall) => ({
id: toolCall.callId,
type: "function",
function: { name: toolCall.name, arguments: JSON.stringify(toolCall.arguments) },
}));
}
messages.push(value);
} else if (message.role === "tool") {
messages.push({
role: "tool",
content: message.parts.length === 0 ? message.content : chatContent(message.parts),
tool_call_id: message.callId,
});
} else {
messages.push({ role: message.role, content: chatContent(message.content) });
}
}
const body: Record<string, JsonValue> = {
model: call.model,
messages,
stream: true,
stream_options: { include_usage: true },
};
if (call.request.tools.length > 0) {
body.tools = call.request.tools.map((tool) => ({
type: "function",
function: { name: tool.name, description: tool.description, parameters: tool.parameters },
}));
}
if (call.request.maxOutputTokens !== null) {
body.max_completion_tokens = call.request.maxOutputTokens;
}
if (call.request.reasoning.effort !== null) {
body.reasoning_effort = call.request.reasoning.effort;
}
if (call.request.latency === "fast") body.service_tier = "fast";
if (call.request.cacheKey !== null) body.prompt_cache_key = call.request.cacheKey;
return { ...body, ...call.extraBody };
}
type ToolState = {
callId: string;
name: string;
arguments: string;
emitted: number;
started: boolean;
};
/** 部分上游会重发完整片段而不是增量;去重后再拼接。 */
function mergeFragment(target: string, fragment: string): string {
if (target === fragment || target.endsWith(fragment)) return target;
if (fragment.startsWith(target)) return fragment;
return target + fragment;
}
function updateTool(
index: number,
callId: string | null,
name: string | null,
argumentsDelta: string | null,
tools: Map<number, ToolState>,
): ModelEvent[] {
let tool = tools.get(index);
if (!tool) {
tool = { callId: "", name: "", arguments: "", emitted: 0, started: false };
tools.set(index, tool);
}
if (callId !== null) tool.callId = mergeFragment(tool.callId, callId);
if (name !== null) tool.name = mergeFragment(tool.name, name);
if (argumentsDelta !== null) tool.arguments += argumentsDelta;
const events: ModelEvent[] = [];
if (!tool.started && tool.callId && tool.name) {
tool.started = true;
events.push({ type: "tool-call-start", index, callId: tool.callId, name: tool.name });
}
if (tool.started && tool.emitted < tool.arguments.length) {
events.push({
type: "tool-call-arguments-delta",
index,
delta: tool.arguments.slice(tool.emitted),
});
tool.emitted = tool.arguments.length;
}
return events;
}
function eventError(value: Record<string, unknown>): string | null {
const error = value.error;
if (error === undefined || error === null) return null;
if (typeof error === "string") return error;
return text(record(error)?.message) ?? JSON.stringify(error);
}
function usageEvent(value: unknown): ModelEvent {
const usage = record(value) ?? {};
return {
type: "usage",
usage: {
inputTokens: count(usage.prompt_tokens),
outputTokens: count(usage.completion_tokens),
totalTokens: count(usage.total_tokens),
cacheReadTokens: count(record(usage.prompt_tokens_details)?.cached_tokens),
cacheWriteTokens: null,
reasoningTokens: count(record(usage.completion_tokens_details)?.reasoning_tokens),
},
};
}
function mapFinish(reason: string, hasTools: boolean): "stop" | "length" | "tool-use" {
if (reason === "tool_calls" || reason === "function_call") return "tool-use";
if (reason === "length") return "length";
if (reason === "stop" || reason === "content_filter") return "stop";
return hasTools ? "tool-use" : "stop";
}
async function readBody(lines: AsyncIterable<string>): Promise<string> {
const collected: string[] = [];
for await (const line of lines) collected.push(line);
return collected.join("\n");
}
/**
* 执行一次 Chat Completions 流式调用,发出与宿主统一事件集一致的标准化事件,
* 包括文本/思考边界与工具参数增量。非 2xx 响应抛出 `HttpError`,
* 流内失败抛出 `Error`,由调用方分类额度与授权问题。
*/
export async function streamOpenAiChat(
call: OpenAiChatCall,
output: ProviderOutput,
context: PluginContext,
): Promise<void> {
const response = await context.network.stream(call.url, {
method: "POST",
headers: {
accept: "text/event-stream",
"content-type": "application/json",
...call.headers,
},
body: JSON.stringify(buildChatBody(call)),
});
if (response.status < 200 || response.status >= 300) {
throw new HttpError(response.status, await readBody(response.lines));
}
let textOpen = false;
let thinkingOpen = false;
let reasoning = "";
const tools = new Map<number, ToolState>();
let finalUsage: ModelEvent | null = null;
let finish: "stop" | "length" | "tool-use" | null = null;
let sawDoneMarker = false;
for await (const line of response.lines) {
if (!line.startsWith("data:")) continue;
const payload = line.slice(5).trim();
if (!payload) continue;
if (payload === "[DONE]") {
sawDoneMarker = true;
break;
}
let value: Record<string, unknown>;
try {
value = record(JSON.parse(payload)) ?? {};
} catch {
throw new Error("OpenAI Chat SSE returned invalid JSON");
}
const error = eventError(value);
if (error !== null) throw new Error(`OpenAI Chat error: ${error}`);
if (value.usage !== undefined && value.usage !== null) {
finalUsage = usageEvent(value.usage);
}
const choice = Array.isArray(value.choices) ? record(value.choices[0]) : null;
if (!choice) continue;
const delta = record(choice.delta) ?? {};
const reasoningDelta = text(delta.reasoning_content) ?? text(delta.reasoning);
if (reasoningDelta) {
if (!thinkingOpen) {
thinkingOpen = true;
output.emit({ type: "thinking-start" });
}
reasoning += reasoningDelta;
output.emit({ type: "thinking-delta", text: reasoningDelta });
}
const content = text(delta.content);
if (content) {
if (thinkingOpen) {
thinkingOpen = false;
output.emit({ type: "thinking-end" });
}
if (!textOpen) {
textOpen = true;
output.emit({ type: "text-start" });
}
output.emit({ type: "text-delta", text: content });
}
if (Array.isArray(delta.tool_calls)) {
for (const [position, rawTool] of delta.tool_calls.entries()) {
const toolDelta = record(rawTool);
if (!toolDelta) continue;
const index = count(toolDelta.index) ?? position;
const fn = record(toolDelta.function) ?? {};
for (
const event of updateTool(
index,
text(toolDelta.id),
text(fn.name),
text(fn.arguments),
tools,
)
) {
output.emit(event);
}
}
}
const finishReason = text(choice.finish_reason);
if (finishReason !== null) finish = mapFinish(finishReason, tools.size > 0);
}
if (thinkingOpen) output.emit({ type: "thinking-end" });
if (textOpen) output.emit({ type: "text-end" });
for (const [index, tool] of tools) {
if (!tool.started) {
if (!tool.name) throw new Error("OpenAI Chat tool call is missing name");
if (!tool.callId) tool.callId = `call-${index}`;
tool.started = true;
output.emit({ type: "tool-call-start", index, callId: tool.callId, name: tool.name });
if (tool.arguments) {
tool.emitted = tool.arguments.length;
output.emit({ type: "tool-call-arguments-delta", index, delta: tool.arguments });
}
}
output.emit({ type: "tool-call-end", index });
}
if (finalUsage !== null) output.emit(finalUsage);
if (reasoning) {
output.emit({
type: "replay-state",
providerKind: REPLAY_KIND,
value: { reasoning_content: reasoning },
});
}
const reason = finish ??
(sawDoneMarker ? (tools.size > 0 ? "tool-use" : "stop") : null);
if (reason === null) {
throw new Error("OpenAI Chat stream ended without finish_reason");
}
output.emit({ type: "done", reason });
}