Add Grok CLI local provider support

This commit is contained in:
musistudio
2026-07-13 08:48:39 +08:00
parent 1ff88a7a3e
commit 0196fdebeb
14 changed files with 1402 additions and 11 deletions
@@ -0,0 +1,764 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import type {
GatewayProviderConfig,
LocalAgentProviderCandidate,
LocalAgentProviderImportResult,
ProviderAccountConfig,
ProviderAccountConnectorConfig,
ProviderAccountMappingConfig,
ProviderModelMetadata
} from "@ccr/core/contracts/app";
import {
bearerAuthPlugin,
firstString,
isRecord,
localAgentProviderApiKey,
missingCandidate,
modelDisplayNamesForModels,
modelMetadataForModels,
providerInternalNamePlaceholder,
providerPayload,
readBoolean,
readJsonRecord,
readString,
uniqueProviderName,
uniqueStrings,
type OAuthTokenSet
} from "@ccr/core/agents/local-providers/shared";
import { fetchWithSystemProxy } from "@ccr/core/proxy/system-proxy-fetch";
import { normalizeProviderBaseUrl } from "@ccr/core/providers/url";
export const grokDefaultBaseUrl = "https://cli-chat-proxy.grok.com/v1";
export const grokDefaultBillingEndpoint = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
export const grokDefaultSubscriptionEndpoint = "https://cli-chat-proxy.grok.com/v1/user?include=subscription";
const grokDefaultModels = ["grok-4.5"];
const grokProviderId = "grok-cli-api";
const grokProviderName = "Grok CLI API";
const grokDefaultOidcIssuer = "https://auth.x.ai";
const grokOauthDefaultTimeoutMs = 8_000;
const grokBillingResetPaths = [
"$.billingPeriodEnd",
"$.currentPeriod.end",
"$.currentPeriod.billingPeriodEnd",
"$.config.billingPeriodEnd",
"$.config.currentPeriod.end",
"$.end"
];
const grokBillingMapping: ProviderAccountMappingConfig = {
meters: [
{
id: "grok_credit_usage_percent",
kind: "quota",
label: "Credit usage",
limit: 100,
remaining: [
"100 - $.creditUsagePercent",
"100 - $.config.creditUsagePercent",
"100 - $.config.creditUsagePercent.val"
],
resetAt: grokBillingResetPaths,
unit: "%",
used: [
"$.creditUsagePercent",
"$.config.creditUsagePercent",
"$.config.creditUsagePercent.val"
],
window: "monthly"
},
{
id: "grok_included_credits",
kind: "quota",
label: "Included credits",
limit: [
"$.monthlyLimit",
"$.monthlyLimit.val",
"$.currentPeriod.monthlyLimit",
"$.currentPeriod.monthlyLimit.val",
"$.config.monthlyLimit",
"$.config.monthlyLimit.val",
"$.config.currentPeriod.monthlyLimit",
"$.config.currentPeriod.monthlyLimit.val"
],
resetAt: grokBillingResetPaths,
unit: "credits",
used: [
"$.includedUsed",
"$.includedUsed.val",
"$.currentPeriod.includedUsed",
"$.currentPeriod.includedUsed.val",
"$.config.includedUsed",
"$.config.includedUsed.val",
"$.config.currentPeriod.includedUsed",
"$.config.currentPeriod.includedUsed.val"
],
window: "monthly"
},
{
id: "grok_total_credits",
kind: "quota",
label: "Total credits",
limit: [
"$.monthlyLimit",
"$.monthlyLimit.val",
"$.currentPeriod.monthlyLimit",
"$.currentPeriod.monthlyLimit.val",
"$.config.monthlyLimit",
"$.config.monthlyLimit.val",
"$.config.currentPeriod.monthlyLimit",
"$.config.currentPeriod.monthlyLimit.val"
],
resetAt: grokBillingResetPaths,
unit: "credits",
used: [
"$.totalUsed",
"$.totalUsed.val",
"$.currentPeriod.totalUsed",
"$.currentPeriod.totalUsed.val",
"$.config.totalUsed",
"$.config.totalUsed.val",
"$.config.currentPeriod.totalUsed",
"$.config.currentPeriod.totalUsed.val"
],
window: "monthly"
},
{
id: "grok_pay_as_you_go_cap",
kind: "quota",
label: "Pay-as-you-go cap",
limit: [
"$.onDemandCap",
"$.onDemandCap.val",
"$.currentPeriod.onDemandCap",
"$.currentPeriod.onDemandCap.val",
"$.config.onDemandCap",
"$.config.onDemandCap.val",
"$.config.currentPeriod.onDemandCap",
"$.config.currentPeriod.onDemandCap.val"
],
resetAt: grokBillingResetPaths,
unit: "credits",
used: [
"$.onDemandUsed",
"$.onDemandUsed.val",
"$.currentPeriod.onDemandUsed",
"$.currentPeriod.onDemandUsed.val",
"$.config.onDemandUsed",
"$.config.onDemandUsed.val",
"$.config.currentPeriod.onDemandUsed",
"$.config.currentPeriod.onDemandUsed.val"
],
window: "monthly"
},
{
id: "grok_prepaid_balance",
kind: "balance",
label: "Prepaid balance",
remaining: [
"$.prepaidBalance",
"$.prepaidBalance.val",
"$.currentPeriod.prepaidBalance",
"$.currentPeriod.prepaidBalance.val",
"$.config.prepaidBalance",
"$.config.prepaidBalance.val",
"$.config.currentPeriod.prepaidBalance",
"$.config.currentPeriod.prepaidBalance.val"
],
resetAt: grokBillingResetPaths,
unit: "credits",
window: "monthly"
}
]
};
export type GrokTokenSet = OAuthTokenSet & {
authRecordKey?: string;
oidcClientId?: string;
oidcIssuer?: string;
expiresAt?: string;
};
type GrokModelCatalog = {
baseUrl: string;
modelDisplayNames?: Record<string, string>;
modelMetadata?: Record<string, ProviderModelMetadata>;
models: string[];
};
export function grokCandidate(): LocalAgentProviderCandidate {
const auth = readGrokAuth();
const catalog = readGrokLocalModelCatalog();
if ((auth?.accessToken && !grokAccessTokenExpired(auth)) || auth?.refreshToken) {
return {
detail: "Grok CLI login detected. Click Import to add it as a gateway provider.",
id: grokProviderId,
importable: true,
kind: "grok",
modelDisplayNames: catalog.modelDisplayNames,
modelMetadata: catalog.modelMetadata,
models: catalog.models,
name: grokProviderName,
protocol: "openai_responses",
sourceFile: auth.sourceFile,
status: "available"
};
}
if (auth?.accessToken || auth?.refreshToken) {
return {
detail: auth.accessToken && grokAccessTokenExpired(auth)
? "Grok CLI login was detected, but the access token is expired. Run grok login again, then rescan."
: "Grok CLI login was detected, but no usable access token was found.",
id: grokProviderId,
importable: false,
kind: "grok",
modelDisplayNames: catalog.modelDisplayNames,
modelMetadata: catalog.modelMetadata,
models: catalog.models,
name: grokProviderName,
protocol: "openai_responses",
sourceFile: auth.sourceFile,
status: "locked"
};
}
return missingCandidate("grok", grokProviderId, grokProviderName, "openai_responses", catalog.models, catalog.modelDisplayNames);
}
export async function importGrokProvider(candidate: LocalAgentProviderCandidate, providerNames: string[]): Promise<LocalAgentProviderImportResult> {
const auth = await resolveGrokAuth();
if (!auth?.accessToken || grokAccessTokenExpired(auth)) {
throw new Error("Grok CLI access token was not found or is expired.");
}
return importGrokProviderWithAuth(candidate, providerNames, auth);
}
export function readGrokAuth(): GrokTokenSet | undefined {
const candidates = grokCredentialFiles()
.flatMap((sourceFile) => readGrokAuthRecords(sourceFile));
return candidates.find((item) => item.accessToken && !grokAccessTokenExpired(item)) ??
candidates.find((item) => item.refreshToken) ??
candidates.find((item) => item.accessToken);
}
export async function resolveGrokAuth(): Promise<GrokTokenSet | undefined> {
const auth = readGrokAuth();
if (!auth?.refreshToken || (auth.accessToken && !grokAccessTokenExpired(auth))) {
return auth;
}
return refreshGrokAuth(auth);
}
export function readGrokLocalModelCatalog(): GrokModelCatalog {
const preferredModel = readGrokDefaultModel();
const catalog = grokModelCatalogFromPayload(readJsonRecord(grokModelsCacheFile()), preferredModel);
const models = uniqueStrings([
preferredModel,
...catalog.models,
...grokDefaultModels
]);
return {
baseUrl: catalog.baseUrl || grokRuntimeDefaultBaseUrl(),
modelDisplayNames: modelDisplayNamesForModels(catalog.modelDisplayNames, models),
modelMetadata: modelMetadataForModels(catalog.modelMetadata, models),
models
};
}
function readGrokAuthRecords(sourceFile: string): GrokTokenSet[] {
const record = readJsonRecord(sourceFile);
if (!record) {
return [];
}
return [
record,
...Object.entries(record)
.filter((entry): entry is [string, Record<string, unknown>] => isRecord(entry[1]))
.map(([key, value]) => ({ ...value, __ccr_auth_record_key: key }))
]
.map((item) => grokAuthFromRecord(item, sourceFile))
.filter((item): item is GrokTokenSet => Boolean(item));
}
function grokAuthFromRecord(record: Record<string, unknown>, sourceFile: string): GrokTokenSet | undefined {
const accessToken =
readString(record.key) ||
readString(record.access_token) ||
readString(record.accessToken) ||
readString(record.token) ||
readString(record.id_token) ||
readString(record.idToken);
const refreshToken =
readString(record.refresh_token) ||
readString(record.refreshToken);
if (!accessToken && !refreshToken) {
return undefined;
}
return {
accessToken,
authRecordKey: readString(record.__ccr_auth_record_key),
expiresAt: readString(record.expires_at) || readString(record.expiresAt),
oidcClientId: readString(record.oidc_client_id) || readString(record.oidcClientId) || readString(process.env.GROK_OIDC_CLIENT_ID),
oidcIssuer: readString(record.oidc_issuer) || readString(record.oidcIssuer) || readString(process.env.GROK_OIDC_ISSUER),
refreshToken,
sourceFile
};
}
export function grokAccessTokenExpired(auth: GrokTokenSet): boolean {
const expiresAtMs = dateMs(auth.expiresAt) ?? jwtExpiresAtMs(auth.accessToken);
return expiresAtMs !== undefined && expiresAtMs <= Date.now() + 60_000;
}
function grokModelCatalogFromPayload(payload: unknown, preferredModel?: string): GrokModelCatalog {
const models: string[] = [];
const modelDisplayNames: Record<string, string> = {};
const modelMetadata: Record<string, ProviderModelMetadata> = {};
const baseUrlsByModel: Record<string, string> = {};
for (const item of grokModelCatalogItems(payload)) {
const info = isRecord(item.value) && isRecord(item.value.info) ? item.value.info : isRecord(item.value) ? item.value : {};
if (readBoolean(info.hidden) || readBoolean(info.supported_in_api) === false || readBoolean(info.supportedInApi) === false) {
continue;
}
const apiBackend = readString(info.api_backend) || readString(info.apiBackend);
if (apiBackend && !apiBackend.toLowerCase().includes("responses")) {
continue;
}
const model = readString(info.model) || readString(info.id) || readString(info.name) || item.key;
if (!model) {
continue;
}
models.push(model);
const displayName = readString(info.display_name) || readString(info.displayName) || readString(info.label) || readString(info.title) || readString(info.name);
if (displayName && displayName !== model) {
modelDisplayNames[model] = displayName;
}
const baseUrl = readString(info.base_url) || readString(info.baseUrl);
if (baseUrl) {
baseUrlsByModel[model] = baseUrl;
}
const metadata = grokModelMetadataFromInfo(info);
if (metadata) {
modelMetadata[model] = metadata;
}
}
const uniqueModels = uniqueStrings(models);
const preferredBaseUrl = preferredModel ? baseUrlsByModel[preferredModel] : undefined;
const baseUrl = preferredBaseUrl || firstString(uniqueModels.map((model) => baseUrlsByModel[model])) || grokRuntimeDefaultBaseUrl();
const filteredModels = uniqueModels.filter((model) => !baseUrlsByModel[model] || baseUrlsByModel[model] === baseUrl);
return {
baseUrl,
modelDisplayNames: modelDisplayNamesForModels(modelDisplayNames, filteredModels),
modelMetadata: modelMetadataForModels(modelMetadata, filteredModels),
models: filteredModels
};
}
function grokModelMetadataFromInfo(info: Record<string, unknown>): ProviderModelMetadata | undefined {
const defaultReasoningLevel = readNullableString(info.reasoning_effort) ?? readNullableString(info.reasoningEffort);
const metadata: ProviderModelMetadata = {
...(defaultReasoningLevel !== undefined ? { defaultReasoningLevel } : {})
};
return Object.keys(metadata).length > 0 ? metadata : undefined;
}
function grokModelCatalogItems(payload: unknown): Array<{ key?: string; value: unknown }> {
if (Array.isArray(payload)) {
return payload.map((value) => ({ value }));
}
if (!isRecord(payload)) {
return [];
}
const models = payload.models;
if (Array.isArray(models)) {
return models.map((value) => ({ value }));
}
if (isRecord(models)) {
return Object.entries(models).map(([key, value]) => ({ key, value }));
}
return [];
}
function readGrokDefaultModel(): string | undefined {
for (const sourceFile of grokConfigFiles()) {
if (!existsSync(sourceFile)) {
continue;
}
try {
const text = readFileSync(sourceFile, "utf8");
const match = text.match(/^\s*default\s*=\s*"([^"]+)"\s*$/m) ?? text.match(/^\s*default\s*=\s*'([^']+)'\s*$/m);
const model = match?.[1]?.trim();
if (model) {
return model;
}
} catch {
continue;
}
}
return undefined;
}
function importGrokProviderWithAuth(
candidate: LocalAgentProviderCandidate,
providerNames: string[],
auth: GrokTokenSet
): LocalAgentProviderImportResult {
const catalog = readGrokLocalModelCatalog();
const provider = providerPayload(
{
...candidate,
modelDisplayNames: catalog.modelDisplayNames,
modelMetadata: catalog.modelMetadata,
models: catalog.models
},
uniqueProviderName(providerNames, grokProviderName),
catalog.baseUrl,
grokProviderAccountConfig()
);
return {
candidate: {
...candidate,
modelDisplayNames: catalog.modelDisplayNames,
modelMetadata: catalog.modelMetadata,
models: catalog.models
},
provider,
providerPlugins: [
grokOauthPlugin("grok-cli-oauth", auth.accessToken ?? ""),
grokOauthPlugin("grok-cli-oauth-internal", auth.accessToken ?? "", providerInternalNamePlaceholder)
]
};
}
export function grokProviderAccountConfig(): ProviderAccountConfig {
return {
connectors: [
{
auth: "provider-api-key",
endpoint: grokBillingEndpoint(),
headers: {
"x-grok-client-identifier": "xai-grok-cli",
"x-grok-client-version": "0.2.93"
},
mapping: grokBillingMapping,
type: "http-json"
},
{
auth: "provider-api-key",
endpoint: grokSubscriptionEndpoint(),
headers: {
"x-grok-client-identifier": "xai-grok-cli",
"x-grok-client-version": "0.2.93"
},
mapping: { meters: [] },
parser: "grok-subscription",
type: "http-json"
}
],
enabled: true
};
}
export function normalizeGrokProviderAccountConfig(provider: GatewayProviderConfig): GatewayProviderConfig {
if (!isLocalGrokProvider(provider) || !shouldUseCurrentGrokAccountConfig(provider.account)) {
return provider;
}
const account = grokProviderAccountConfig();
return {
...provider,
account: {
...account,
refreshIntervalMs: provider.account?.refreshIntervalMs ?? account.refreshIntervalMs
}
};
}
function isLocalGrokProvider(provider: GatewayProviderConfig): boolean {
if (providerApiKey(provider) !== localAgentProviderApiKey) {
return false;
}
const baseUrl = normalizeProviderBaseUrl(providerBaseUrl(provider)).toLowerCase();
const name = provider.name?.toLowerCase() ?? "";
return baseUrl.includes("cli-chat-proxy.grok.com") || name.includes("grok");
}
function shouldUseCurrentGrokAccountConfig(account: ProviderAccountConfig | undefined): boolean {
if (account?.enabled === false) {
return false;
}
const connectors = account?.connectors ?? [];
if (connectors.length === 0) {
return true;
}
return connectors.every(isGrokAccountConnector);
}
function isGrokAccountConnector(connector: ProviderAccountConnectorConfig): boolean {
if (connector.type === "standard") {
return !connector.endpoint?.trim() && !connector.endpoints?.length && !connector.headers && !connector.id;
}
if (connector.type !== "http-json") {
return false;
}
return /^https:\/\/grok\.com\/(?:billing|user)(?:$|[?#/])/i.test(connector.endpoint.trim()) ||
/^https:\/\/cli-chat-proxy\.grok\.com\/v1\/(?:billing|user)(?:$|[?#/])/i.test(connector.endpoint.trim());
}
function providerBaseUrl(provider: GatewayProviderConfig): string {
return provider.api_base_url || provider.baseurl || provider.baseUrl || "";
}
function providerApiKey(provider: GatewayProviderConfig): string {
return provider.api_key || provider.apiKey || provider.apikey || "";
}
function grokOauthPlugin(suffix: string, token: string, providerName?: string): Record<string, unknown> {
return {
...bearerAuthPlugin(suffix, token, {}, providerName),
request: {
headers: {
"x-grok-model-override": "{{ model }}"
},
strict: true
}
};
}
async function refreshGrokAuth(auth: GrokTokenSet): Promise<GrokTokenSet> {
const refreshToken = auth.refreshToken;
if (!refreshToken) {
throw new Error("Grok CLI refresh token was not found.");
}
const clientId = auth.oidcClientId || readString(process.env.GROK_OIDC_CLIENT_ID);
if (!clientId) {
throw new Error("Grok CLI OAuth client id was not found.");
}
const tokenEndpoint = await grokTokenEndpoint(auth);
const timeoutMs = normalizeGrokOauthTimeout(process.env.GROK_OIDC_REFRESH_TIMEOUT_MS);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetchWithSystemProxy(tokenEndpoint, {
body: new URLSearchParams({
client_id: clientId,
grant_type: "refresh_token",
refresh_token: refreshToken
}).toString(),
headers: {
"content-type": "application/x-www-form-urlencoded"
},
method: "POST",
signal: controller.signal
});
const text = await response.text();
const payload = parseJsonRecord(text);
if (!response.ok) {
throw new Error(`Grok CLI OAuth token refresh returned HTTP ${response.status}${tokenRefreshErrorMessage(payload, text)}`);
}
const accessToken = readString(payload?.access_token) || readString(payload?.accessToken);
if (!accessToken) {
throw new Error("Grok CLI OAuth token refresh did not return an access token.");
}
const refreshed: GrokTokenSet = {
...auth,
accessToken,
expiresAt: refreshedGrokExpiresAt(accessToken, payload),
refreshToken: readString(payload?.refresh_token) || readString(payload?.refreshToken) || refreshToken
};
persistRefreshedGrokAuth(refreshed);
return refreshed;
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
throw new Error(`Grok CLI OAuth token refresh timed out after ${timeoutMs}ms.`);
}
throw error;
} finally {
clearTimeout(timer);
}
}
async function grokTokenEndpoint(auth: GrokTokenSet): Promise<string> {
const configured = readString(process.env.GROK_OIDC_TOKEN_ENDPOINT);
if (configured) {
return configured;
}
const issuer = (auth.oidcIssuer || readString(process.env.GROK_OIDC_ISSUER) || grokDefaultOidcIssuer).replace(/\/+$/, "");
const metadataUrl = `${issuer}/.well-known/openid-configuration`;
const timeoutMs = normalizeGrokOauthTimeout(process.env.GROK_OIDC_REFRESH_TIMEOUT_MS);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetchWithSystemProxy(metadataUrl, {
headers: { accept: "application/json" },
signal: controller.signal
});
const text = await response.text();
const payload = parseJsonRecord(text);
if (!response.ok) {
throw new Error(`Grok CLI OIDC discovery returned HTTP ${response.status}${tokenRefreshErrorMessage(payload, text)}`);
}
const tokenEndpoint = readString(payload?.token_endpoint) || readString(payload?.tokenEndpoint);
if (!tokenEndpoint) {
throw new Error("Grok CLI OIDC discovery did not return a token endpoint.");
}
return tokenEndpoint;
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
throw new Error(`Grok CLI OIDC discovery timed out after ${timeoutMs}ms.`);
}
throw error;
} finally {
clearTimeout(timer);
}
}
function grokCredentialFiles(): string[] {
const explicitFile = process.env.GROK_AUTH_FILE?.trim();
return uniqueStrings([
explicitFile,
path.join(grokStorageRoot(), "auth.json"),
path.join(grokStorageRoot(), "credentials.json")
]);
}
function grokConfigFiles(): string[] {
const explicitFile = process.env.GROK_CONFIG_FILE?.trim();
return uniqueStrings([
explicitFile,
path.join(grokStorageRoot(), "config.toml")
]);
}
function grokModelsCacheFile(): string {
return process.env.GROK_MODELS_CACHE_FILE?.trim() || path.join(grokStorageRoot(), "models_cache.json");
}
function grokStorageRoot(): string {
const explicitRoot = process.env.GROK_HOME?.trim() || process.env.GROK_STORAGE_DIR?.trim() || process.env.GROK_CONFIG_DIR?.trim();
if (explicitRoot) {
return explicitRoot;
}
const homeDir = process.env.CCR_INTERNAL_HOME_DIR?.trim() || process.env.HOME?.trim() || process.env.USERPROFILE?.trim() || os.homedir();
return path.join(homeDir, ".grok");
}
function grokRuntimeDefaultBaseUrl(): string {
return process.env.GROK_CLI_CHAT_PROXY_BASE_URL?.trim() || grokDefaultBaseUrl;
}
function grokBillingEndpoint(): string {
return process.env.GROK_BILLING_ENDPOINT?.trim() || grokDefaultBillingEndpoint;
}
function grokSubscriptionEndpoint(): string {
return process.env.GROK_SUBSCRIPTION_ENDPOINT?.trim() || grokDefaultSubscriptionEndpoint;
}
function readNullableString(value: unknown): string | null | undefined {
if (value === null) {
return null;
}
return readString(value) || undefined;
}
function dateMs(value: string | undefined): number | undefined {
if (!value) {
return undefined;
}
const timestamp = new Date(value).getTime();
return Number.isFinite(timestamp) ? timestamp : undefined;
}
function jwtExpiresAtMs(token: string | undefined): number | undefined {
const encoded = token?.split(".")[1];
if (!encoded) {
return undefined;
}
try {
const padded = encoded.padEnd(encoded.length + ((4 - encoded.length % 4) % 4), "=");
const payload = JSON.parse(Buffer.from(padded.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8")) as unknown;
const exp = isRecord(payload) && typeof payload.exp === "number" ? payload.exp : undefined;
return exp ? exp * 1000 : undefined;
} catch {
return undefined;
}
}
function refreshedGrokExpiresAt(accessToken: string, payload: Record<string, unknown> | undefined): string | undefined {
const expiresAtMs = jwtExpiresAtMs(accessToken) ?? expiresInMs(payload?.expires_in) ?? expiresInMs(payload?.expiresIn);
return expiresAtMs ? new Date(expiresAtMs).toISOString() : undefined;
}
function expiresInMs(value: unknown): number | undefined {
const seconds = typeof value === "number"
? value
: typeof value === "string" && value.trim()
? Number(value)
: undefined;
return seconds && Number.isFinite(seconds) ? Date.now() + seconds * 1000 : undefined;
}
function persistRefreshedGrokAuth(auth: GrokTokenSet): void {
if (!auth.sourceFile || !auth.accessToken) {
return;
}
try {
const parsed = JSON.parse(readFileSync(auth.sourceFile, "utf8")) as unknown;
if (!isRecord(parsed)) {
return;
}
let target: Record<string, unknown> = parsed;
if (auth.authRecordKey) {
const authRecord = parsed[auth.authRecordKey];
if (isRecord(authRecord)) {
target = authRecord;
}
}
target.key = auth.accessToken;
if (auth.refreshToken) {
target.refresh_token = auth.refreshToken;
}
if (auth.expiresAt) {
target.expires_at = auth.expiresAt;
}
writeFileSync(auth.sourceFile, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
} catch {
// Best effort. The refreshed token is still used for this CCR run.
}
}
function parseJsonRecord(text: string): Record<string, unknown> | undefined {
try {
const payload = JSON.parse(text) as unknown;
return isRecord(payload) ? payload : undefined;
} catch {
return undefined;
}
}
function tokenRefreshErrorMessage(payload: Record<string, unknown> | undefined, text: string): string {
const message =
readString(payload?.error_description) ||
readString(payload?.error) ||
readString(payload?.message) ||
readableResponseSnippet(text);
return message ? `: ${message}` : "";
}
function readableResponseSnippet(text: string): string {
return text.replace(/\s+/g, " ").trim().slice(0, 200);
}
function normalizeGrokOauthTimeout(value: unknown): number {
const numeric = Number(value);
return Math.max(1, Number.isFinite(numeric) ? numeric : grokOauthDefaultTimeoutMs);
}
export function grokModelCatalogFromPayloadForTest(payload: unknown, preferredModel?: string): GrokModelCatalog {
return grokModelCatalogFromPayload(payload, preferredModel);
}
@@ -7,9 +7,11 @@ import type {
} from "@ccr/core/contracts/app";
import { claudeCodeCandidate, importClaudeCodeProvider } from "@ccr/core/agents/local-providers/claude-code";
import { codexCandidate, importCodexProvider, probeCodexProvider } from "@ccr/core/agents/local-providers/codex";
import { grokCandidate, importGrokProvider } from "@ccr/core/agents/local-providers/grok";
import { importZcodeProvider, zcodeCandidate } from "@ccr/core/agents/local-providers/zcode";
export { codexDefaultBaseUrl, readCodexAuth } from "@ccr/core/agents/local-providers/codex";
export { grokDefaultBaseUrl, readGrokAuth, resolveGrokAuth } from "@ccr/core/agents/local-providers/grok";
export { readZcodeLocalProviderCredential, zcodeDefaultBaseUrl } from "@ccr/core/agents/local-providers/zcode";
export { localAgentProviderApiKey, type OAuthTokenSet } from "@ccr/core/agents/local-providers/shared";
@@ -17,6 +19,7 @@ export function getLocalAgentProviderCandidates(): LocalAgentProviderCandidate[]
return [
codexCandidate(),
claudeCodeCandidate(),
grokCandidate(),
zcodeCandidate()
].filter((candidate) => candidate.status !== "missing");
}
@@ -36,6 +39,9 @@ export async function importLocalAgentProvider(request: LocalAgentProviderImport
if (candidate.kind === "claude-code") {
return importClaudeCodeProvider(candidate, request.providerNames ?? []);
}
if (candidate.kind === "grok") {
return importGrokProvider(candidate, request.providerNames ?? []);
}
return importZcodeProvider(candidate, request.providerNames ?? []);
}
+2 -1
View File
@@ -4,6 +4,7 @@ import { loadPersistedAppConfig, replacePersistedAppConfig } from "@ccr/core/con
import { loadPersistedApiKeys, replacePersistedApiKeys } from "@ccr/core/config/api-key-store";
import { CONFIG_FILE, GATEWAY_CONFIG_FILE, LEGACY_CONFIG_FILE, LEGACY_WINDOWS_CONFIG_FILE } from "@ccr/core/config/constants";
import { normalizeCodexProviderAccountConfig } from "@ccr/core/agents/local-providers/codex";
import { normalizeGrokProviderAccountConfig } from "@ccr/core/agents/local-providers/grok";
import { CLAUDE_CODE_DEFAULT_ENV, CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV, DEFAULT_OVERVIEW_WIDGETS, DEFAULT_TRAY_COMPONENT_VARIANTS, DEFAULT_TRAY_WIDGETS, DEFAULT_TRAY_WINDOW_MODULES, OVERVIEW_WIDGET_SIZE_VALUES, ROUTER_FALLBACK_MAX_RETRY_COUNT, TRAY_SINGLETON_WIDGET_TYPES, TRAY_TOP_WIDGET_TYPES, TRAY_WINDOW_MODULE_IDS, enforceSingleEnabledGlobalProfilePerAgent } from "@ccr/core/contracts/app";
import { createDefaultAppConfig } from "@ccr/core/config/default-config";
import { findProviderPresetByBaseUrl, providerApiKeySafetyIssue, providerEndpointCanReceiveProviderApiKey } from "@ccr/core/providers/presets/index";
@@ -1067,7 +1068,7 @@ function parseProviders(value: unknown): GatewayProviderConfig[] | undefined {
transformer: item.transformer,
type: readString(item.type)
};
return normalizeCodexProviderAccountConfig(provider);
return normalizeGrokProviderAccountConfig(normalizeCodexProviderAccountConfig(provider));
})
.filter((item): item is GatewayProviderConfig => Boolean(item));
+2 -2
View File
@@ -175,7 +175,7 @@ export type ProviderAccountStatus = "ok" | "warning" | "critical" | "error" | "u
export type ProviderAccountMeterKind = "balance" | "subscription" | "quota" | "time_window" | "tokens" | "requests";
export type ProviderAccountMeterUnit = "USD" | "CNY" | "hours" | "minutes" | "tokens" | "requests" | string;
export type ProviderAccountMeterWindow = "5h" | "daily" | "weekly" | "monthly" | string;
export type ProviderAccountHttpJsonParser = "kimi-code-usages" | "new-api-key-usage" | "new-api-user-self";
export type ProviderAccountHttpJsonParser = "grok-subscription" | "kimi-code-usages" | "new-api-key-usage" | "new-api-user-self";
export type ProviderAccountConfig = {
connectors?: ProviderAccountConnectorConfig[];
@@ -329,7 +329,7 @@ export type ProviderManifestFetchResult = {
url: string;
};
export type LocalAgentProviderKind = "claude-code" | "codex" | "zcode";
export type LocalAgentProviderKind = "claude-code" | "codex" | "grok" | "zcode";
export type LocalAgentProviderStatus = "available" | "locked" | "missing";
+37 -3
View File
@@ -42,7 +42,8 @@ import { normalizeProviderBaseUrl as normalizeProviderBaseUrlInput } from "@ccr/
import { backendService } from "@ccr/core/plugins/backend-service";
import { RAW_TRACE_SPOOL_DIR } from "@ccr/core/config/constants";
import { loadPersistedApiKeys } from "@ccr/core/config/api-key-store";
import { codexDefaultBaseUrl, readCodexAuth } from "@ccr/core/agents/local-providers/service";
import { codexDefaultBaseUrl, readCodexAuth, readGrokAuth, resolveGrokAuth } from "@ccr/core/agents/local-providers/service";
import { grokAccessTokenExpired } from "@ccr/core/agents/local-providers/grok";
import { fetchWithSystemProxy, getSystemProxyUrlForProtocol } from "@ccr/core/proxy/system-proxy-fetch";
import { handleNetworkCaptureMcpRequest, isNetworkCaptureMcpPath } from "@ccr/core/mcp/network-capture-mcp";
import { BROWSER_AUTOMATION_MCP_PATH, TOOL_HUB_MCP_SERVER_NAME, browserAutomationMcpEnabled, toolHubBuiltInBackendServers, toolHubMcpRuntimeConfig, toolHubRequestTimeoutMs } from "@ccr/core/mcp/toolhub-config";
@@ -1176,10 +1177,10 @@ async function writeCoreGatewayConfig(
assertLoopbackCoreHost(config.gateway.coreHost);
mkdirSync(dirname(config.gateway.generatedConfigFile), { mode: privateDirMode, recursive: true });
const pluginCoreGatewayConfig = pluginService.getCoreGatewayConfig();
const providerPlugins = withCodexOauthRuntimeDefaults([
const providerPlugins = await withGrokOauthRuntimeDefaults(withCodexOauthRuntimeDefaults([
...(config.providerPlugins ?? []).filter(providerPluginEnabled),
...pluginService.getCoreProviderPlugins().filter(providerPluginEnabled)
]);
]));
const codexOauthProviderNames = codexOauthLocalProviderNames(providerPlugins);
const virtualModelProfiles = normalizeCoreGatewayVirtualModelProfiles(withCodexCompatibleVirtualModelProfiles(withFusionVirtualModelAliases([
...(config.virtualModelProfiles ?? []),
@@ -1398,6 +1399,31 @@ function withCodexOauthRuntimeDefaults(providerPlugins: unknown[]): unknown[] {
});
}
async function withGrokOauthRuntimeDefaults(providerPlugins: unknown[]): Promise<unknown[]> {
const grokAuth = await resolveGrokAuth().catch(() => readGrokAuth());
if (!grokAuth?.accessToken || grokAccessTokenExpired(grokAuth)) {
return providerPlugins;
}
return providerPlugins.map((plugin) => {
if (!isLocalGrokOauthProviderPlugin(plugin)) {
return plugin;
}
const currentAuth = isRecord(plugin.auth) ? plugin.auth : {};
const currentHeaders = isRecord(currentAuth.headers) ? currentAuth.headers : {};
return {
...plugin,
auth: {
...currentAuth,
headers: {
...currentHeaders,
authorization: `Bearer ${grokAuth.accessToken}`
}
}
};
});
}
function codexOauthLocalProviderNames(providerPlugins: unknown[]): Set<string> {
const names = new Set<string>();
for (const plugin of providerPlugins) {
@@ -1455,6 +1481,14 @@ function isLocalCodexOauthProviderPlugin(value: unknown): value is Record<string
return key.startsWith("ccr-local-agent-") && key.includes("codex-oauth");
}
function isLocalGrokOauthProviderPlugin(value: unknown): value is Record<string, unknown> {
if (!isRecord(value)) {
return false;
}
const key = stringValue(value.key)?.toLowerCase() ?? "";
return key.startsWith("ccr-local-agent-") && key.includes("grok-cli-oauth");
}
function withCodexBackendRequestTransform(request: unknown): Record<string, unknown> {
const currentRequest = isRecord(request) ? request : {};
const bodyRemove = Array.isArray(currentRequest.bodyRemove)
@@ -5,9 +5,12 @@ import {
codexDefaultBaseUrl,
localAgentProviderApiKey,
readCodexAuth,
readGrokAuth,
resolveGrokAuth,
readZcodeLocalProviderCredential,
zcodeDefaultBaseUrl
} from "@ccr/core/agents/local-providers/service";
import { grokAccessTokenExpired } from "@ccr/core/agents/local-providers/grok";
import { pluginService } from "@ccr/core/plugins/service";
import { getUsageTotalsSince } from "@ccr/core/usage/store";
import { findProviderPresetByBaseUrl, providerEndpointCanReceiveProviderApiKey } from "@ccr/core/providers/presets/index";
@@ -163,6 +166,16 @@ export async function testProviderAccountConnector(request: ProviderAccountTestR
type: "http-json"
};
const payload = await fetchJson(connector.endpoint, provider, connector.auth, connector.headers, connector.method, connector.body);
if (connector.parser === "grok-subscription") {
const meters = grokSubscriptionMeters(payload);
return {
meters,
message: grokSubscriptionMessage(payload),
paths: flattenJsonPaths(payload),
payload,
status: grokSubscriptionStatus(payload) ?? statusFromMeters(meters, [], 1)
};
}
if (connector.parser === "kimi-code-usages") {
const meters = kimiCodeUsageMeters(payload);
return {
@@ -647,6 +660,15 @@ async function resolveHttpJsonConnector(
...(connector.headers ?? {}),
...(request.headers ?? {})
}, connector.method, connector.body);
if (connector.parser === "grok-subscription") {
return {
errors: [],
message: grokSubscriptionMessage(payload),
meters: grokSubscriptionMeters(payload),
source: "http-json",
status: grokSubscriptionStatus(payload)
};
}
if (connector.parser === "kimi-code-usages") {
const meters = kimiCodeUsageMeters(payload);
return {
@@ -846,6 +868,110 @@ function normalizeRemoteSnapshot(
};
}
function grokSubscriptionMeters(payload: unknown): ProviderAccountMeter[] {
const allowAccess = grokSubscriptionBoolean(payload, [
"allow_access",
"allowAccess",
"has_grok_code_access",
"hasGrokCodeAccess"
]);
if (allowAccess === undefined) {
return [];
}
return [
{
id: "grok_subscription_access",
kind: "subscription",
label: "Subscription access",
limit: 100,
remaining: allowAccess ? 100 : 0,
source: "http-json",
unit: "%",
used: allowAccess ? 0 : 100,
window: "subscription"
}
];
}
function grokSubscriptionMessage(payload: unknown): string | undefined {
return grokSubscriptionString(payload, [
"gate_message",
"gateMessage",
"subscription_tier_display",
"subscriptionTierDisplay",
"subscription_tier",
"subscriptionTier",
"tier_display",
"tierDisplay",
"tier",
"user_blocked_reason",
"userBlockedReason",
"team_blocked_reason",
"teamBlockedReason"
]);
}
function grokSubscriptionStatus(payload: unknown): ProviderAccountStatus | undefined {
const allowAccess = grokSubscriptionBoolean(payload, [
"allow_access",
"allowAccess",
"has_grok_code_access",
"hasGrokCodeAccess"
]);
if (allowAccess === false) {
return "critical";
}
if (grokSubscriptionString(payload, ["gate_message", "gateMessage", "gate_label", "gateLabel"])) {
return "warning";
}
return undefined;
}
function grokSubscriptionBoolean(payload: unknown, keys: string[]): boolean | undefined {
for (const record of grokSubscriptionRecords(payload)) {
for (const key of keys) {
const value = readBoolean(readJsonRecordValue(record, key));
if (value !== undefined) {
return value;
}
}
}
return undefined;
}
function grokSubscriptionString(payload: unknown, keys: string[]): string | undefined {
for (const record of grokSubscriptionRecords(payload)) {
for (const key of keys) {
const value = readString(readJsonRecordValue(record, key));
if (value) {
return value;
}
}
}
return undefined;
}
function grokSubscriptionRecords(payload: unknown): Record<string, unknown>[] {
if (!isRecord(payload)) {
return [];
}
const records: Record<string, unknown>[] = [];
const queue = [payload];
for (const record of queue) {
if (records.includes(record)) {
continue;
}
records.push(record);
for (const key of ["account", "data", "meta", "subscription", "user", "viewer_context", "viewerContext"]) {
const nested = readJsonRecordValue(record, key);
if (isRecord(nested)) {
queue.push(nested);
}
}
}
return records;
}
function newApiKeyUsageMeters(payload: unknown): ProviderAccountMeter[] {
const meter = newApiKeyUsageMeter(payload);
return meter ? [meter] : [];
@@ -1252,6 +1378,9 @@ async function localAgentProviderAccountCredential(
if (key.includes("claude-code-oauth")) {
return localBearerAccountCredential(plugin);
}
if (key.includes("grok-cli-oauth")) {
return await localGrokAccountCredential(plugin);
}
if (key.includes("zcode-api-key")) {
return localApiKeyHeaderAccountCredential(plugin);
}
@@ -1573,6 +1702,18 @@ function localBearerAccountCredential(plugin: Record<string, unknown>): { apiKey
};
}
async function localGrokAccountCredential(plugin: Record<string, unknown>): Promise<{ apiKey?: string; headers?: Record<string, string> }> {
const headers = localProviderPluginAuthHeaders(plugin);
const auth = await resolveGrokAuth().catch(() => readGrokAuth());
const apiKey = auth?.accessToken && !grokAccessTokenExpired(auth)
? auth.accessToken
: readBearerToken(headers.authorization || headers.Authorization);
return {
apiKey,
headers: withoutHeader(headers, "authorization")
};
}
function localApiKeyHeaderAccountCredential(plugin: Record<string, unknown>): { apiKey?: string; headers?: Record<string, string> } {
const headers = localProviderPluginAuthHeaders(plugin);
const apiKey = headers["x-api-key"] || headers["X-API-Key"];
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -1189,7 +1189,7 @@ function LocalAgentProviderImportPanel({
apiKey: result.provider.apiKey ?? "",
baseUrl: result.provider.baseUrl,
credentials: [],
icon: result.provider.icon ?? "",
icon: result.provider.icon?.trim() || localAgentProviderIconUrls[candidate.kind] || "",
modelDescriptions: result.provider.modelDescriptions,
modelDisplayNames: result.provider.modelDisplayNames,
modelMetadata: result.provider.modelMetadata,
@@ -1214,7 +1214,7 @@ function LocalAgentProviderImportPanel({
<div className="mb-2 flex min-w-0 items-center justify-between gap-2">
<div className="min-w-0">
<div className="truncate text-[12px] font-semibold text-foreground">{t("Import local agent login")}</div>
<div className="mt-0.5 text-[11px] leading-4 text-muted-foreground">{t("CCR scanned this computer for Claude Code, Codex, and ZCode login states. Click Import to add one as a gateway provider.")}</div>
<div className="mt-0.5 text-[11px] leading-4 text-muted-foreground">{t("CCR scanned this computer for Claude Code, Codex, Grok CLI, and ZCode login states. Click Import to add one as a gateway provider.")}</div>
</div>
{loading ? <LoaderCircle className="h-4 w-4 shrink-0 animate-spin text-muted-foreground" /> : null}
</div>
@@ -1281,6 +1281,7 @@ const localAgentProviderApiKey = "ccr-local-agent-login";
const localAgentProviderPluginSuffixes: Record<LocalAgentProviderCandidate["kind"], string[]> = {
"claude-code": ["-claude-code-oauth", "-claude-code-oauth-internal"],
codex: ["-codex-oauth", "-codex-oauth-internal"],
grok: ["-grok-cli-oauth", "-grok-cli-oauth-internal"],
zcode: ["-zcode-api-key", "-zcode-api-key-internal"]
};
@@ -729,6 +729,7 @@ export function providerPluginCapability(item: Record<string, unknown>): string
const capabilities: string[] = ["Provider middleware"];
if (item.deepseekThinking || item.deepSeekThinking) capabilities.push("DeepSeek thinking");
if (item.codexOauth) capabilities.push("Codex OAuth");
if (typeof item.key === "string" && item.key.includes("grok-cli-oauth")) capabilities.push("Grok OAuth");
if (item.auth) capabilities.push("Auth mutation");
if (item.request) capabilities.push("Request mutation");
if (item.response) capabilities.push("Response mutation");
+8 -2
View File
@@ -259,7 +259,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Checking connection": "Checking connection",
"Click Check Connection to verify connectivity with a real model request.": "Click Check Connection to verify connectivity with a real model request.",
"Connection verified": "Connection verified",
"CCR scanned this computer for Claude Code, Codex, and ZCode login states. Click Import to add one as a gateway provider.": "CCR scanned this computer for Claude Code, Codex, and ZCode login states. Click Import to add one as a gateway provider.",
"CCR scanned this computer for Claude Code, Codex, Grok CLI, and ZCode login states. Click Import to add one as a gateway provider.": "CCR scanned this computer for Claude Code, Codex, Grok CLI, and ZCode login states. Click Import to add one as a gateway provider.",
"Detected": "Detected",
"Detecting protocols": "Detecting protocols",
"Enter API endpoint, API key, and at least one model to enable connectivity check.": "Enter API endpoint, API key, and at least one model to enable connectivity check.",
@@ -270,6 +270,9 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Claude Code login was detected, but no usable access token was found.": "Claude Code login was detected, but no usable access token was found.",
"Codex auth file was found, but no usable token was detected.": "Codex auth file was found, but no usable token was detected.",
"Claude Code credential file was found, but no usable OAuth token was detected.": "Claude Code credential file was found, but no usable OAuth token was detected.",
"Grok CLI login detected. Click Import to add it as a gateway provider.": "Grok CLI login detected. Click Import to add it as a gateway provider.",
"Grok CLI login was detected, but no usable access token was found.": "Grok CLI login was detected, but no usable access token was found.",
"Grok CLI login was detected, but the access token is expired. Run grok login again, then rescan.": "Grok CLI login was detected, but the access token is expired. Run grok login again, then rescan.",
"Locked": "Locked",
"Local agent login will be connected after saving this provider.": "Local agent login will be connected after saving this provider.",
"Models to check": "Models to check",
@@ -770,7 +773,7 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Default failure handling": "默认故障处理",
"Default on failure": "默认失败处理",
"Description": "描述",
"CCR scanned this computer for Claude Code, Codex, and ZCode login states. Click Import to add one as a gateway provider.": "CCR 已扫描本机的 Claude Code、Codex 和 ZCode 登录态。点击导入即可添加为网关供应商。",
"CCR scanned this computer for Claude Code, Codex, Grok CLI, and ZCode login states. Click Import to add one as a gateway provider.": "CCR 已扫描本机的 Claude Code、Codex、Grok CLI 和 ZCode 登录态。点击导入即可添加为网关供应商。",
"Detected": "已检测",
"Detecting protocols": "正在探测协议",
"Enter API endpoint, API key, and at least one model to enable connectivity check.": "填写 API 地址、API Key 和至少一个模型后,才可检测连通性。",
@@ -780,6 +783,9 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
"Claude Code login was detected, but no usable access token was found.": "已检测到 Claude Code 登录态,但没有找到可用的 access token。",
"Codex auth file was found, but no usable token was detected.": "已找到 Codex 认证文件,但没有检测到可用 token。",
"Claude Code credential file was found, but no usable OAuth token was detected.": "已找到 Claude Code 凭据文件,但没有检测到可用 OAuth token。",
"Grok CLI login detected. Click Import to add it as a gateway provider.": "已检测到 Grok CLI 登录态。点击导入即可添加为网关供应商。",
"Grok CLI login was detected, but no usable access token was found.": "已检测到 Grok CLI 登录态,但没有找到可用的 access token。",
"Grok CLI login was detected, but the access token is expired. Run grok login again, then rescan.": "已检测到 Grok CLI 登录态,但 access token 已过期。请重新运行 grok login,然后重新扫描。",
"Locked": "已加密",
"Local agent login will be connected after saving this provider.": "保存这个供应商后会接入本机 Agent 登录态。",
"Display name": "显示名称",
@@ -102,6 +102,7 @@ import { cn } from "@/lib/utils";
import appLogoUrl from "@/assets/logo.png";
import claudeCodeLogoUrl from "@/assets/agent-logos/claude-code.png";
import codexLogoUrl from "@/assets/agent-logos/codex.png";
import grokLogoUrl from "@/assets/agent-logos/grok.ico";
import zcodeLogoUrl from "@/assets/agent-logos/zcode.png";
import onboardingMascotSpriteUrl from "@/assets/onboarding/mascot-transition.svg";
import anthropicProviderIconUrl from "@/assets/provider-icons/anthropic.png";
@@ -388,9 +389,12 @@ import type { AddProviderDraft, AddRoutingRuleDraft, ModelCatalogItem, ProviderC
export const localAgentProviderIconUrls: Record<LocalAgentProviderKind, string> = {
"claude-code": claudeCodeLogoUrl,
codex: codexLogoUrl,
grok: grokLogoUrl,
zcode: zcodeLogoUrl
};
const localAgentProviderApiKeyValue = "ccr-local-agent-login";
export function createModelCatalogItems(config: AppConfig): ModelCatalogItem[] {
const rows: ModelCatalogItem[] = [];
config.Providers.forEach((provider, providerIndex) => {
@@ -774,6 +778,9 @@ export function providerDeepLinkDisplayIcon(payload: ProviderDeepLinkPayload): s
}
export function providerDisplayIcon(provider: GatewayProviderConfig): string {
if (isLocalGrokProvider(provider)) {
return grokLogoUrl;
}
const icon = provider.icon?.trim();
if (icon) {
return icon;
@@ -783,6 +790,15 @@ export function providerDisplayIcon(provider: GatewayProviderConfig): string {
return preset ? providerPresetIconUrls[preset.id] ?? "" : "";
}
function isLocalGrokProvider(provider: GatewayProviderConfig): boolean {
if (providerApiKey(provider) !== localAgentProviderApiKeyValue) {
return false;
}
const baseUrl = normalizeProviderBaseUrl(providerBaseUrl(provider)).toLowerCase();
const name = provider.name?.toLowerCase() ?? "";
return baseUrl.includes("cli-chat-proxy.grok.com") || name.includes("grok");
}
export type ProviderDeepLinkCatalogModelsResolution = {
modelDisplayNames?: Record<string, string>;
modelMetadata?: Record<string, ProviderModelMetadata>;
@@ -0,0 +1,320 @@
import assert from "node:assert/strict";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import {
grokCandidate,
grokDefaultBillingEndpoint,
grokDefaultBaseUrl,
grokDefaultSubscriptionEndpoint,
grokModelCatalogFromPayloadForTest,
importGrokProvider,
normalizeGrokProviderAccountConfig
} from "../../packages/core/src/agents/local-providers/grok.ts";
import { localAgentProviderApiKey } from "../../packages/core/src/agents/local-providers/shared.ts";
test("Grok local provider imports bearer token and model override plugin", async () => {
await withGrokHome(async (grokHome) => {
writeGrokAuth(grokHome, {
key: "grok-access-token",
refresh_token: "grok-refresh-token",
expires_at: "2999-01-01T00:00:00Z"
});
writeFileSync(path.join(grokHome, "config.toml"), "[models]\ndefault = \"grok-4.5\"\n");
writeGrokModels(grokHome);
const candidate = grokCandidate();
assert.equal(candidate.kind, "grok");
assert.equal(candidate.importable, true);
assert.equal(candidate.protocol, "openai_responses");
assert.deepEqual(candidate.models, ["grok-4.5", "grok-composer-2.5-fast"]);
assert.deepEqual(candidate.modelDisplayNames, {
"grok-4.5": "Grok 4.5",
"grok-composer-2.5-fast": "Composer 2.5"
});
const result = await importGrokProvider(candidate, []);
assert.equal(result.provider.name, "Grok CLI API");
assert.equal(result.provider.baseUrl, grokDefaultBaseUrl);
assert.equal(result.provider.protocol, "openai_responses");
assert.equal(result.provider.apiKey, "ccr-local-agent-login");
assert.equal(result.provider.account?.enabled, true);
assert.equal(result.provider.account?.connectors?.length, 2);
assert.equal(result.provider.account?.connectors?.[0]?.type, "http-json");
assert.equal(result.provider.account?.connectors?.[0]?.auth, "provider-api-key");
assert.equal(result.provider.account?.connectors?.[0]?.endpoint, grokDefaultBillingEndpoint);
assert.equal(result.provider.account?.connectors?.[0]?.headers?.["x-grok-client-identifier"], "xai-grok-cli");
assert.equal(result.provider.account?.connectors?.[0]?.headers?.["x-grok-client-version"], "0.2.93");
assert.deepEqual(
result.provider.account?.connectors?.[0]?.mapping.meters.map((meter) => meter.id),
[
"grok_credit_usage_percent",
"grok_included_credits",
"grok_total_credits",
"grok_pay_as_you_go_cap",
"grok_prepaid_balance"
]
);
assert.equal(result.provider.account?.connectors?.[1]?.type, "http-json");
assert.equal(result.provider.account?.connectors?.[1]?.auth, "provider-api-key");
assert.equal(result.provider.account?.connectors?.[1]?.endpoint, grokDefaultSubscriptionEndpoint);
assert.equal(result.provider.account?.connectors?.[1]?.headers?.["x-grok-client-identifier"], "xai-grok-cli");
assert.equal(result.provider.account?.connectors?.[1]?.parser, "grok-subscription");
assert.equal(result.providerPlugins.length, 2);
assert.equal(result.providerPlugins[0].auth.headers.authorization, "Bearer grok-access-token");
assert.equal(result.providerPlugins[0].request.headers["x-grok-model-override"], "{{ model }}");
assert.equal(result.providerPlugins[1].providerName, "__CCR_PROVIDER_INTERNAL_NAME__");
});
});
test("Grok local provider refreshes expired token during import", async (t) => {
await withGrokHome(async (grokHome) => {
writeGrokAuth(grokHome, {
key: "expired-token",
refresh_token: "grok-refresh-token",
expires_at: "2000-01-01T00:00:00Z",
oidc_client_id: "grok-client-id",
oidc_issuer: "https://auth.x.ai"
});
writeGrokModels(grokHome);
const previousFetch = globalThis.fetch;
const previousTokenEndpoint = process.env.GROK_OIDC_TOKEN_ENDPOINT;
process.env.GROK_OIDC_TOKEN_ENDPOINT = "http://127.0.0.1/grok/oauth/token";
let requestBody = "";
globalThis.fetch = async (input, init) => {
assert.equal(String(input), "http://127.0.0.1/grok/oauth/token");
requestBody = String(init?.body ?? "");
return new Response(JSON.stringify({
access_token: "refreshed-grok-access-token",
expires_in: 3600,
refresh_token: "refreshed-grok-refresh-token"
}), { headers: { "content-type": "application/json" }, status: 200 });
};
t.after(() => {
globalThis.fetch = previousFetch;
restoreEnv("GROK_OIDC_TOKEN_ENDPOINT", previousTokenEndpoint);
});
const candidate = grokCandidate();
assert.equal(candidate.kind, "grok");
assert.equal(candidate.importable, true);
assert.equal(candidate.status, "available");
const result = await importGrokProvider(candidate, []);
assert.equal(result.providerPlugins[0].auth.headers.authorization, "Bearer refreshed-grok-access-token");
assert.equal(requestBody, "client_id=grok-client-id&grant_type=refresh_token&refresh_token=grok-refresh-token");
const persisted = JSON.parse(readFileSync(path.join(grokHome, "auth.json"), "utf8"));
assert.equal(persisted["https://auth.x.ai::test-account"].key, "refreshed-grok-access-token");
assert.equal(persisted["https://auth.x.ai::test-account"].refresh_token, "refreshed-grok-refresh-token");
});
});
test("Grok model catalog parser keeps responses models from the selected base URL", () => {
const catalog = grokModelCatalogFromPayloadForTest({
models: {
"grok-4.5": {
info: {
api_backend: "responses",
base_url: "https://cli-chat-proxy.grok.com/v1",
context_window: 500000,
model: "grok-4.5",
name: "Grok 4.5",
reasoning_effort: "high",
supported_in_api: true
}
},
"grok-hidden": {
info: {
api_backend: "responses",
base_url: "https://cli-chat-proxy.grok.com/v1",
hidden: true,
model: "grok-hidden",
name: "Hidden"
}
},
"grok-chat": {
info: {
api_backend: "chat_completions",
base_url: "https://cli-chat-proxy.grok.com/v1",
model: "grok-chat",
name: "Chat"
}
},
"other-responses": {
info: {
api_backend: "responses",
base_url: "https://example.com/v1",
model: "other-responses",
name: "Other"
}
}
}
}, "grok-4.5");
assert.deepEqual(catalog.models, ["grok-4.5"]);
assert.deepEqual(catalog.modelDisplayNames, { "grok-4.5": "Grok 4.5" });
assert.deepEqual(catalog.modelMetadata, { "grok-4.5": { defaultReasoningLevel: "high" } });
assert.equal(catalog.baseUrl, grokDefaultBaseUrl);
});
test("Grok local provider account config upgrades persisted usage mapping", () => {
const provider = normalizeGrokProviderAccountConfig({
account: {
connectors: [],
refreshIntervalMs: 45000
},
api_base_url: grokDefaultBaseUrl,
api_key: localAgentProviderApiKey,
models: ["grok-4.5"],
name: "Grok CLI API",
protocol: "openai_responses"
});
const connector = provider.account?.connectors?.[0];
const subscriptionConnector = provider.account?.connectors?.[1];
assert.equal(provider.account?.refreshIntervalMs, 45000);
assert.equal(connector?.type, "http-json");
assert.equal(connector?.endpoint, grokDefaultBillingEndpoint);
assert.equal(connector?.mapping.meters.find((meter) => meter.id === "grok_credit_usage_percent")?.unit, "%");
assert.equal(subscriptionConnector?.type, "http-json");
assert.equal(subscriptionConnector?.endpoint, grokDefaultSubscriptionEndpoint);
assert.equal(subscriptionConnector?.parser, "grok-subscription");
});
test("Grok local provider account config upgrades old web usage endpoints", () => {
const provider = normalizeGrokProviderAccountConfig({
account: {
connectors: [
{
endpoint: "https://grok.com/billing?format=credits",
mapping: { meters: [] },
type: "http-json"
},
{
endpoint: "https://grok.com/user?include=subscription",
mapping: { meters: [] },
parser: "grok-subscription",
type: "http-json"
}
]
},
api_base_url: grokDefaultBaseUrl,
api_key: localAgentProviderApiKey,
models: ["grok-4.5"],
name: "Grok CLI API",
protocol: "openai_responses"
});
assert.equal(provider.account?.connectors?.[0]?.type, "http-json");
assert.equal(provider.account?.connectors?.[0]?.endpoint, grokDefaultBillingEndpoint);
assert.equal(provider.account?.connectors?.[1]?.type, "http-json");
assert.equal(provider.account?.connectors?.[1]?.endpoint, grokDefaultSubscriptionEndpoint);
});
test("Grok local provider account config keeps custom connectors", () => {
const account = {
connectors: [
{
endpoint: "https://example.com/usage",
mapping: {
meters: [
{
id: "custom",
kind: "balance",
label: "Custom",
remaining: "$.balance",
unit: "credits"
}
]
},
type: "http-json"
}
],
enabled: true
};
const provider = normalizeGrokProviderAccountConfig({
account,
api_base_url: grokDefaultBaseUrl,
api_key: localAgentProviderApiKey,
models: ["grok-4.5"],
name: "Grok CLI API",
protocol: "openai_responses"
});
assert.equal(provider.account, account);
});
async function withGrokHome(run) {
const previousGrokHome = process.env.GROK_HOME;
const previousGrokAuthFile = process.env.GROK_AUTH_FILE;
const previousGrokConfigFile = process.env.GROK_CONFIG_FILE;
const previousGrokModelsCacheFile = process.env.GROK_MODELS_CACHE_FILE;
const previousGrokTokenEndpoint = process.env.GROK_OIDC_TOKEN_ENDPOINT;
const grokHome = mkdtempSync(path.join(os.tmpdir(), "ccr-grok-test-"));
process.env.GROK_HOME = grokHome;
delete process.env.GROK_AUTH_FILE;
delete process.env.GROK_CONFIG_FILE;
delete process.env.GROK_MODELS_CACHE_FILE;
delete process.env.GROK_OIDC_TOKEN_ENDPOINT;
try {
await run(grokHome);
} finally {
restoreEnv("GROK_HOME", previousGrokHome);
restoreEnv("GROK_AUTH_FILE", previousGrokAuthFile);
restoreEnv("GROK_CONFIG_FILE", previousGrokConfigFile);
restoreEnv("GROK_MODELS_CACHE_FILE", previousGrokModelsCacheFile);
restoreEnv("GROK_OIDC_TOKEN_ENDPOINT", previousGrokTokenEndpoint);
rmSync(grokHome, { force: true, recursive: true });
}
}
function restoreEnv(name, value) {
if (value === undefined) {
delete process.env[name];
} else {
process.env[name] = value;
}
}
function writeGrokAuth(grokHome, auth) {
writeFileSync(path.join(grokHome, "auth.json"), JSON.stringify({
"https://auth.x.ai::test-account": auth
}, null, 2));
}
function writeGrokModels(grokHome) {
writeFileSync(path.join(grokHome, "models_cache.json"), JSON.stringify({
models: {
"grok-4.5": {
info: {
api_backend: "responses",
auth_scheme: "bearer",
base_url: grokDefaultBaseUrl,
context_window: 500000,
hidden: false,
model: "grok-4.5",
name: "Grok 4.5",
reasoning_effort: "high",
supported_in_api: true
}
},
"grok-composer-2.5-fast": {
info: {
api_backend: "responses",
auth_scheme: "bearer",
base_url: grokDefaultBaseUrl,
context_window: 200000,
hidden: false,
model: "grok-composer-2.5-fast",
name: "Composer 2.5",
reasoning_effort: null,
supported_in_api: true
}
}
}
}, null, 2));
}
+90 -1
View File
@@ -5,13 +5,102 @@ import path from "node:path";
import test from "node:test";
import {
localAgentProviderAccountCredentialForTest,
localCodexAccountCredentialForTest
localCodexAccountCredentialForTest,
testProviderAccountConnector
} from "../../packages/core/src/providers/account-service.ts";
import {
grokDefaultBillingEndpoint,
grokDefaultBaseUrl,
grokDefaultSubscriptionEndpoint,
grokProviderAccountConfig
} from "../../packages/core/src/agents/local-providers/grok.ts";
const localAgentProviderApiKey = "ccr-local-agent-login";
const codexDefaultBaseUrl = "https://chatgpt.com/backend-api/codex";
const zcodeDefaultBaseUrl = "https://zcode.z.ai/api/v1/zcode-plan/anthropic";
test("Grok billing connector maps credit usage payload", async (t) => {
const previousFetch = globalThis.fetch;
let authorization = "";
let clientIdentifier = "";
let clientVersion = "";
globalThis.fetch = async (input, init) => {
assert.equal(String(input), grokDefaultBillingEndpoint);
authorization = init?.headers?.authorization ?? "";
clientIdentifier = init?.headers?.["x-grok-client-identifier"] ?? "";
clientVersion = init?.headers?.["x-grok-client-version"] ?? "";
return new Response(JSON.stringify({
config: {
billingPeriodEnd: "2026-08-01T00:00:00Z",
creditUsagePercent: { val: 25 },
includedUsed: { val: 10 },
monthlyLimit: { val: 40 },
onDemandCap: { val: 100 },
onDemandUsed: { val: 5 },
prepaidBalance: { val: 12 },
totalUsed: { val: 15 }
}
}), { headers: { "content-type": "application/json" }, status: 200 });
};
t.after(() => {
globalThis.fetch = previousFetch;
});
const connector = grokProviderAccountConfig().connectors?.[0];
assert.equal(connector?.type, "http-json");
const result = await testProviderAccountConnector({
apiKey: "grok-access-token",
baseUrl: grokDefaultBaseUrl,
connector,
providerName: "Grok CLI API"
});
assert.equal(authorization, "Bearer grok-access-token");
assert.equal(clientIdentifier, "xai-grok-cli");
assert.equal(clientVersion, "0.2.93");
assert.equal(result.meters.find((meter) => meter.id === "grok_credit_usage_percent")?.remaining, 75);
assert.equal(result.meters.find((meter) => meter.id === "grok_included_credits")?.remaining, 30);
assert.equal(result.meters.find((meter) => meter.id === "grok_total_credits")?.used, 15);
assert.equal(result.meters.find((meter) => meter.id === "grok_pay_as_you_go_cap")?.remaining, 95);
assert.equal(result.meters.find((meter) => meter.id === "grok_prepaid_balance")?.remaining, 12);
});
test("Grok subscription connector maps access status payload", async (t) => {
const previousFetch = globalThis.fetch;
let authorization = "";
let clientIdentifier = "";
let clientVersion = "";
globalThis.fetch = async (input, init) => {
assert.equal(String(input), grokDefaultSubscriptionEndpoint);
authorization = init?.headers?.authorization ?? "";
clientIdentifier = init?.headers?.["x-grok-client-identifier"] ?? "";
clientVersion = init?.headers?.["x-grok-client-version"] ?? "";
return new Response(JSON.stringify({
hasGrokCodeAccess: true,
subscriptionTier: "SuperGrok Heavy"
}), { headers: { "content-type": "application/json" }, status: 200 });
};
t.after(() => {
globalThis.fetch = previousFetch;
});
const connector = grokProviderAccountConfig().connectors?.[1];
assert.equal(connector?.type, "http-json");
const result = await testProviderAccountConnector({
apiKey: "grok-access-token",
baseUrl: grokDefaultBaseUrl,
connector,
providerName: "Grok CLI API"
});
assert.equal(authorization, "Bearer grok-access-token");
assert.equal(clientIdentifier, "xai-grok-cli");
assert.equal(clientVersion, "0.2.93");
assert.equal(result.status, "ok");
assert.equal(result.message, "SuperGrok Heavy");
assert.equal(result.meters.find((meter) => meter.id === "grok_subscription_access")?.remaining, 100);
});
test("Codex local account credential refreshes when only a refresh token is available", async (t) => {
const previousHome = process.env.CCR_INTERNAL_HOME_DIR;
const home = mkdtempSync(path.join(os.tmpdir(), "ccr-codex-account-refresh-"));
+12
View File
@@ -12,6 +12,7 @@ import {
createProviderConfigFromDeepLink,
createProviderDraft,
createProviderInstallLinkFromDraft,
localAgentProviderIconUrls,
providerCapabilitiesForProtocols,
providerCapabilityBaseUrlForProtocol,
providerDisplayIcon,
@@ -467,6 +468,17 @@ test("provider display icon prefers custom icons and falls back to preset icons"
}),
providerPresetIconUrls.gemini
);
assert.equal(
providerDisplayIcon({
api_base_url: "https://cli-chat-proxy.grok.com/v1",
api_key: "ccr-local-agent-login",
icon: "/assets/grok-old.svg",
models: [],
name: "Grok CLI API",
type: "openai_responses"
}),
localAgentProviderIconUrls.grok
);
});
test("ProvidersView renders configured provider icons in the list", () => {