@gajae-code/ai 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/usage/grok-cli.d.ts +10 -0
- package/dist/types/utils/oauth/xai.d.ts +10 -3
- package/package.json +2 -2
- package/src/auth-storage.ts +3 -0
- package/src/index.ts +1 -0
- package/src/models.json +24 -0
- package/src/provider-models/descriptors.ts +1 -1
- package/src/rate-limit-utils.ts +11 -2
- package/src/usage/grok-cli.ts +163 -0
- package/src/utils/oauth/xai.ts +49 -13
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.5.1] - 2026-06-14
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Classified model/message limit exhaustion as persistent usage-limit errors so hosts fail fast or switch credentials instead of leaving sessions in an unbounded retry/working state.
|
|
10
|
+
|
|
5
11
|
## [0.5.0] - 2026-06-13
|
|
6
12
|
|
|
7
13
|
### Added
|
package/dist/types/index.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export * from "./usage/claude";
|
|
|
33
33
|
export * from "./usage/gemini";
|
|
34
34
|
export * from "./usage/github-copilot";
|
|
35
35
|
export * from "./usage/google-antigravity";
|
|
36
|
+
export * from "./usage/grok-cli";
|
|
36
37
|
export * from "./usage/kimi";
|
|
37
38
|
export * from "./usage/minimax-code";
|
|
38
39
|
export * from "./usage/openai-codex";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CredentialRankingStrategy, UsageProvider } from "../usage";
|
|
2
|
+
interface BillingUsage {
|
|
3
|
+
monthlyLimit: number;
|
|
4
|
+
used: number;
|
|
5
|
+
billingPeriodEnd: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function parseGrokCliBillingUsage(payload: unknown): BillingUsage;
|
|
8
|
+
export declare const grokCliUsageProvider: UsageProvider;
|
|
9
|
+
export declare const grokCliRankingStrategy: CredentialRankingStrategy;
|
|
10
|
+
export {};
|
|
@@ -8,16 +8,23 @@ interface XaiDiscovery {
|
|
|
8
8
|
authorizationEndpoint: string;
|
|
9
9
|
tokenEndpoint: string;
|
|
10
10
|
}
|
|
11
|
+
export interface XaiOAuthFlowOptions {
|
|
12
|
+
extraAuthorizeParams?: Readonly<Record<string, string>>;
|
|
13
|
+
}
|
|
14
|
+
export interface XaiOAuthRefreshOptions {
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
extraTokenParams?: Readonly<Record<string, string>>;
|
|
17
|
+
}
|
|
11
18
|
export declare function discoverXaiOAuthEndpoints(signal?: AbortSignal): Promise<XaiDiscovery>;
|
|
12
19
|
export declare class XaiOAuthFlow extends OAuthCallbackFlow {
|
|
13
20
|
#private;
|
|
14
|
-
constructor(ctrl: OAuthController);
|
|
21
|
+
constructor(ctrl: OAuthController, options?: XaiOAuthFlowOptions);
|
|
15
22
|
generateAuthUrl(state: string, redirectUri: string): Promise<{
|
|
16
23
|
url: string;
|
|
17
24
|
instructions?: string;
|
|
18
25
|
}>;
|
|
19
26
|
exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials>;
|
|
20
27
|
}
|
|
21
|
-
export declare function loginXai(ctrl: OAuthController): Promise<OAuthCredentials>;
|
|
22
|
-
export declare function refreshXaiToken(refreshToken: string,
|
|
28
|
+
export declare function loginXai(ctrl: OAuthController, options?: XaiOAuthFlowOptions): Promise<OAuthCredentials>;
|
|
29
|
+
export declare function refreshXaiToken(refreshToken: string, options?: AbortSignal | XaiOAuthRefreshOptions): Promise<OAuthCredentials>;
|
|
23
30
|
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.1",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://gaebal-gajae.dev",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
45
45
|
"@bufbuild/protobuf": "^2.12.0",
|
|
46
|
-
"@gajae-code/utils": "0.5.
|
|
46
|
+
"@gajae-code/utils": "0.5.1",
|
|
47
47
|
"openai": "^6.36.0",
|
|
48
48
|
"partial-json": "^0.1.7",
|
|
49
49
|
"zod": "4.4.3"
|
package/src/auth-storage.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { claudeRankingStrategy, claudeUsageProvider } from "./usage/claude";
|
|
|
27
27
|
import { googleGeminiCliUsageProvider } from "./usage/gemini";
|
|
28
28
|
import { githubCopilotUsageProvider } from "./usage/github-copilot";
|
|
29
29
|
import { antigravityUsageProvider } from "./usage/google-antigravity";
|
|
30
|
+
import { grokCliRankingStrategy, grokCliUsageProvider } from "./usage/grok-cli";
|
|
30
31
|
import { kimiUsageProvider } from "./usage/kimi";
|
|
31
32
|
import { codexRankingStrategy, openaiCodexUsageProvider } from "./usage/openai-codex";
|
|
32
33
|
import { zaiUsageProvider } from "./usage/zai";
|
|
@@ -370,6 +371,7 @@ const DEFAULT_USAGE_PROVIDERS: UsageProvider[] = [
|
|
|
370
371
|
claudeUsageProvider,
|
|
371
372
|
zaiUsageProvider,
|
|
372
373
|
githubCopilotUsageProvider,
|
|
374
|
+
grokCliUsageProvider,
|
|
373
375
|
];
|
|
374
376
|
|
|
375
377
|
const DEFAULT_USAGE_PROVIDER_MAP = new Map<Provider, UsageProvider>(
|
|
@@ -498,6 +500,7 @@ function resolveDefaultUsageProvider(provider: Provider): UsageProvider | undefi
|
|
|
498
500
|
const DEFAULT_RANKING_STRATEGIES = new Map<Provider, CredentialRankingStrategy>([
|
|
499
501
|
["openai-codex", codexRankingStrategy],
|
|
500
502
|
["anthropic", claudeRankingStrategy],
|
|
503
|
+
["grok-build", grokCliRankingStrategy],
|
|
501
504
|
]);
|
|
502
505
|
|
|
503
506
|
function resolveDefaultRankingStrategy(provider: Provider): CredentialRankingStrategy | undefined {
|
package/src/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ export * from "./usage/claude";
|
|
|
33
33
|
export * from "./usage/gemini";
|
|
34
34
|
export * from "./usage/github-copilot";
|
|
35
35
|
export * from "./usage/google-antigravity";
|
|
36
|
+
export * from "./usage/grok-cli";
|
|
36
37
|
export * from "./usage/kimi";
|
|
37
38
|
export * from "./usage/minimax-code";
|
|
38
39
|
export * from "./usage/openai-codex";
|
package/src/models.json
CHANGED
|
@@ -71048,6 +71048,30 @@
|
|
|
71048
71048
|
"maxLevel": "xhigh"
|
|
71049
71049
|
}
|
|
71050
71050
|
},
|
|
71051
|
+
"glm-5.2": {
|
|
71052
|
+
"id": "glm-5.2",
|
|
71053
|
+
"name": "GLM-5.2",
|
|
71054
|
+
"api": "anthropic-messages",
|
|
71055
|
+
"provider": "zai",
|
|
71056
|
+
"baseUrl": "https://api.z.ai/api/anthropic",
|
|
71057
|
+
"reasoning": true,
|
|
71058
|
+
"input": [
|
|
71059
|
+
"text"
|
|
71060
|
+
],
|
|
71061
|
+
"cost": {
|
|
71062
|
+
"input": 0,
|
|
71063
|
+
"output": 0,
|
|
71064
|
+
"cacheRead": 0,
|
|
71065
|
+
"cacheWrite": 0
|
|
71066
|
+
},
|
|
71067
|
+
"contextWindow": 200000,
|
|
71068
|
+
"maxTokens": 131072,
|
|
71069
|
+
"thinking": {
|
|
71070
|
+
"mode": "budget",
|
|
71071
|
+
"minLevel": "minimal",
|
|
71072
|
+
"maxLevel": "xhigh"
|
|
71073
|
+
}
|
|
71074
|
+
},
|
|
71051
71075
|
"glm-5v-turbo": {
|
|
71052
71076
|
"id": "glm-5v-turbo",
|
|
71053
71077
|
"name": "GLM-5V-Turbo",
|
|
@@ -280,7 +280,7 @@ export const PROVIDER_DESCRIPTORS: readonly ProviderDescriptor[] = [
|
|
|
280
280
|
config => zenmuxModelManagerOptions(config),
|
|
281
281
|
catalog("ZenMux", ["ZENMUX_API_KEY"]),
|
|
282
282
|
),
|
|
283
|
-
catalogDescriptor("zai", "glm-5.
|
|
283
|
+
catalogDescriptor("zai", "glm-5.2", config => zaiModelManagerOptions(config), catalog("zAI", ["ZAI_API_KEY"])),
|
|
284
284
|
descriptor("github-copilot", "gpt-4o", config => githubCopilotModelManagerOptions(config)),
|
|
285
285
|
descriptor("google", "gemini-2.5-pro", config => googleModelManagerOptions(config)),
|
|
286
286
|
catalogDescriptor(
|
package/src/rate-limit-utils.ts
CHANGED
|
@@ -45,7 +45,16 @@ export function parseRateLimitReason(errorMessage: string): RateLimitReason {
|
|
|
45
45
|
return "RATE_LIMIT_EXCEEDED";
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
if (
|
|
48
|
+
if (
|
|
49
|
+
lower.includes("exhausted") ||
|
|
50
|
+
lower.includes("quota") ||
|
|
51
|
+
lower.includes("usage limit") ||
|
|
52
|
+
lower.includes("model limit") ||
|
|
53
|
+
lower.includes("model_limit") ||
|
|
54
|
+
lower.includes("message limit") ||
|
|
55
|
+
lower.includes("message_limit") ||
|
|
56
|
+
lower.includes("limit for this model")
|
|
57
|
+
) {
|
|
49
58
|
return "QUOTA_EXHAUSTED";
|
|
50
59
|
}
|
|
51
60
|
|
|
@@ -77,7 +86,7 @@ export function calculateRateLimitBackoffMs(reason: RateLimitReason): number {
|
|
|
77
86
|
|
|
78
87
|
/** Detect usage/quota limit errors in error messages (persistent, requires credential switch). */
|
|
79
88
|
const USAGE_LIMIT_PATTERN =
|
|
80
|
-
/usage.?limit|usage_limit_reached|usage_not_included|limit_reached|quota.?exceeded|resource
|
|
89
|
+
/usage.?limit|usage_limit_reached|usage_not_included|limit_reached|model.?limit|model_limit_reached|message.?limit|message_limit_reached|limit for this model|quota.?exceeded|resource has been exhausted[^\n]*(?:quota|limit)/i;
|
|
81
90
|
|
|
82
91
|
export function isUsageLimitError(errorMessage: string): boolean {
|
|
83
92
|
return USAGE_LIMIT_PATTERN.test(errorMessage);
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CredentialRankingStrategy,
|
|
3
|
+
UsageFetchContext,
|
|
4
|
+
UsageFetchParams,
|
|
5
|
+
UsageLimit,
|
|
6
|
+
UsageProvider,
|
|
7
|
+
UsageReport,
|
|
8
|
+
} from "../usage";
|
|
9
|
+
|
|
10
|
+
interface BillingUsage {
|
|
11
|
+
monthlyLimit: number;
|
|
12
|
+
used: number;
|
|
13
|
+
billingPeriodEnd: string;
|
|
14
|
+
}
|
|
15
|
+
const DEFAULT_GROK_BUILD_BASE_URL = "https://cli-chat-proxy.grok.com/v1";
|
|
16
|
+
const ALLOWED_GROK_BUILD_HOSTS = new Set(["cli-chat-proxy.grok.com"]);
|
|
17
|
+
|
|
18
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
19
|
+
return !!value && typeof value === "object";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function finiteNumber(value: unknown): number | undefined {
|
|
23
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseValNumber(value: unknown): number | undefined {
|
|
27
|
+
return isRecord(value) ? finiteNumber(value.val) : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function parseGrokCliBillingUsage(payload: unknown): BillingUsage {
|
|
31
|
+
if (!isRecord(payload) || !isRecord(payload.config)) {
|
|
32
|
+
throw new Error("invalid Grok CLI billing payload");
|
|
33
|
+
}
|
|
34
|
+
const monthlyLimit = parseValNumber(payload.config.monthlyLimit);
|
|
35
|
+
const used = parseValNumber(payload.config.used);
|
|
36
|
+
const billingPeriodEnd = payload.config.billingPeriodEnd;
|
|
37
|
+
if (
|
|
38
|
+
monthlyLimit === undefined ||
|
|
39
|
+
used === undefined ||
|
|
40
|
+
typeof billingPeriodEnd !== "string" ||
|
|
41
|
+
!Number.isFinite(new Date(billingPeriodEnd).getTime())
|
|
42
|
+
) {
|
|
43
|
+
throw new Error("invalid Grok CLI billing payload");
|
|
44
|
+
}
|
|
45
|
+
return { monthlyLimit, used, billingPeriodEnd };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isAllowedGrokCredentialHost(baseUrl: string): boolean {
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(baseUrl);
|
|
51
|
+
return url.protocol === "https:" && ALLOWED_GROK_BUILD_HOSTS.has(url.hostname.toLowerCase());
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeGrokBaseUrl(baseUrl?: string): string {
|
|
58
|
+
const normalized = (baseUrl?.trim() || DEFAULT_GROK_BUILD_BASE_URL).replace(/\/+$/, "");
|
|
59
|
+
return isAllowedGrokCredentialHost(normalized) ? normalized : DEFAULT_GROK_BUILD_BASE_URL;
|
|
60
|
+
}
|
|
61
|
+
function isUnsafeGrokBaseUrlOverride(baseUrl?: string): boolean {
|
|
62
|
+
const normalized = baseUrl?.trim().replace(/\/+$/, "");
|
|
63
|
+
return !!normalized && !isAllowedGrokCredentialHost(normalized);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolveAccessToken(params: UsageFetchParams): string | undefined {
|
|
67
|
+
const token = params.credential.accessToken ?? params.credential.apiKey ?? process.env.GROK_CLI_OAUTH_TOKEN;
|
|
68
|
+
return token?.trim() || undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildMonthlyUsageLimit(usage: BillingUsage, nowMs: number): UsageLimit {
|
|
72
|
+
const usedFraction = usage.monthlyLimit > 0 ? usage.used / usage.monthlyLimit : 0;
|
|
73
|
+
const percent = usedFraction * 100;
|
|
74
|
+
const resetsAt = new Date(usage.billingPeriodEnd).getTime();
|
|
75
|
+
return {
|
|
76
|
+
id: "grok-build:7d",
|
|
77
|
+
label: "SuperGrok monthly credits",
|
|
78
|
+
scope: { provider: "grok-build", shared: true, windowId: "7d" },
|
|
79
|
+
window: {
|
|
80
|
+
id: "7d",
|
|
81
|
+
label: "Monthly credits",
|
|
82
|
+
resetsAt,
|
|
83
|
+
},
|
|
84
|
+
amount: {
|
|
85
|
+
unit: "percent",
|
|
86
|
+
used: percent,
|
|
87
|
+
limit: 100,
|
|
88
|
+
remaining: Math.max(0, 100 - percent),
|
|
89
|
+
usedFraction,
|
|
90
|
+
remainingFraction: Math.max(0, 1 - usedFraction),
|
|
91
|
+
},
|
|
92
|
+
status: percent >= 95 ? "exhausted" : percent >= 80 ? "warning" : "ok",
|
|
93
|
+
notes: [
|
|
94
|
+
`${usage.used}/${usage.monthlyLimit} credits used`,
|
|
95
|
+
`resets in ${Math.max(0, Math.round((resetsAt - nowMs) / 3_600_000))}h`,
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const grokCliUsageProvider: UsageProvider = {
|
|
101
|
+
id: "grok-build",
|
|
102
|
+
|
|
103
|
+
supports(params) {
|
|
104
|
+
return params.provider === "grok-build";
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
async fetchUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null> {
|
|
108
|
+
const accessToken = resolveAccessToken(params);
|
|
109
|
+
if (!accessToken) {
|
|
110
|
+
ctx.logger?.warn("Grok Build usage: no access token", { provider: params.provider });
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (isUnsafeGrokBaseUrlOverride(params.baseUrl)) {
|
|
115
|
+
ctx.logger?.warn("Grok Build usage: ignoring unsafe base URL override for credential safety", {
|
|
116
|
+
provider: params.provider,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const billingBaseUrl = normalizeGrokBaseUrl(params.baseUrl);
|
|
120
|
+
const response = await ctx.fetch(`${billingBaseUrl}/billing`, {
|
|
121
|
+
headers: {
|
|
122
|
+
Authorization: `Bearer ${accessToken}`,
|
|
123
|
+
"x-xai-token-auth": "xai-grok-cli",
|
|
124
|
+
accept: "application/json",
|
|
125
|
+
},
|
|
126
|
+
signal: params.signal,
|
|
127
|
+
});
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
ctx.logger?.warn("Grok Build billing request failed", { status: response.status, provider: params.provider });
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const payload = (await response.json()) as unknown;
|
|
134
|
+
let billing: BillingUsage;
|
|
135
|
+
try {
|
|
136
|
+
billing = parseGrokCliBillingUsage(payload);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
ctx.logger?.warn("Grok Build billing parse failed", { error: String(error) });
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const nowMs = Date.now();
|
|
143
|
+
return {
|
|
144
|
+
provider: "grok-build",
|
|
145
|
+
fetchedAt: nowMs,
|
|
146
|
+
limits: [buildMonthlyUsageLimit(billing, nowMs)],
|
|
147
|
+
metadata: {
|
|
148
|
+
email: params.credential.email,
|
|
149
|
+
accountId: params.credential.accountId,
|
|
150
|
+
subscription: true,
|
|
151
|
+
},
|
|
152
|
+
raw: payload,
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const grokCliRankingStrategy: CredentialRankingStrategy = {
|
|
158
|
+
findWindowLimits(report) {
|
|
159
|
+
const monthly = report.limits.find(limit => limit.id === "grok-build:7d");
|
|
160
|
+
return { secondary: monthly };
|
|
161
|
+
},
|
|
162
|
+
windowDefaults: { primaryMs: 5 * 60 * 60 * 1000, secondaryMs: 30 * 24 * 60 * 60 * 1000 },
|
|
163
|
+
};
|
package/src/utils/oauth/xai.ts
CHANGED
|
@@ -30,6 +30,15 @@ interface XaiTokenPayload {
|
|
|
30
30
|
token_type?: unknown;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export interface XaiOAuthFlowOptions {
|
|
34
|
+
extraAuthorizeParams?: Readonly<Record<string, string>>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface XaiOAuthRefreshOptions {
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
extraTokenParams?: Readonly<Record<string, string>>;
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
interface XaiJwtPayload {
|
|
34
43
|
sub?: unknown;
|
|
35
44
|
email?: unknown;
|
|
@@ -41,6 +50,28 @@ function requestSignal(signal: AbortSignal | undefined): AbortSignal {
|
|
|
41
50
|
return signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
42
51
|
}
|
|
43
52
|
|
|
53
|
+
function addNonOverridingParams(
|
|
54
|
+
target: URLSearchParams | Record<string, string>,
|
|
55
|
+
params: Readonly<Record<string, string>>,
|
|
56
|
+
): void {
|
|
57
|
+
for (const [key, value] of Object.entries(params)) {
|
|
58
|
+
if (key.length === 0 || value.length === 0) continue;
|
|
59
|
+
if (target instanceof URLSearchParams) {
|
|
60
|
+
if (!target.has(key)) target.set(key, value);
|
|
61
|
+
} else if (!(key in target)) {
|
|
62
|
+
target[key] = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isAbortSignal(value: AbortSignal | XaiOAuthRefreshOptions | undefined): value is AbortSignal {
|
|
68
|
+
return value instanceof AbortSignal;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveRefreshOptions(options: AbortSignal | XaiOAuthRefreshOptions | undefined): XaiOAuthRefreshOptions {
|
|
72
|
+
return isAbortSignal(options) ? { signal: options } : (options ?? {});
|
|
73
|
+
}
|
|
74
|
+
|
|
44
75
|
function validateXaiEndpoint(rawUrl: string): string {
|
|
45
76
|
const parsed = new URL(rawUrl);
|
|
46
77
|
const host = parsed.hostname.toLowerCase();
|
|
@@ -136,8 +167,9 @@ function credentialsFromTokenPayload(payload: XaiTokenPayload, refreshFallback =
|
|
|
136
167
|
export class XaiOAuthFlow extends OAuthCallbackFlow {
|
|
137
168
|
#verifier = "";
|
|
138
169
|
#discovery: XaiDiscovery | undefined;
|
|
170
|
+
#extraAuthorizeParams: Readonly<Record<string, string>>;
|
|
139
171
|
|
|
140
|
-
constructor(ctrl: OAuthController) {
|
|
172
|
+
constructor(ctrl: OAuthController, options: XaiOAuthFlowOptions = {}) {
|
|
141
173
|
super(ctrl, {
|
|
142
174
|
preferredPort: XAI_OAUTH_CALLBACK_PORT,
|
|
143
175
|
callbackPath: XAI_OAUTH_CALLBACK_PATH,
|
|
@@ -145,6 +177,7 @@ export class XaiOAuthFlow extends OAuthCallbackFlow {
|
|
|
145
177
|
callbackBindHostname: "127.0.0.1",
|
|
146
178
|
redirectUri: `http://127.0.0.1:${XAI_OAUTH_CALLBACK_PORT}${XAI_OAUTH_CALLBACK_PATH}`,
|
|
147
179
|
} satisfies OAuthCallbackFlowOptions);
|
|
180
|
+
this.#extraAuthorizeParams = options.extraAuthorizeParams ?? {};
|
|
148
181
|
}
|
|
149
182
|
|
|
150
183
|
async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
|
|
@@ -161,6 +194,7 @@ export class XaiOAuthFlow extends OAuthCallbackFlow {
|
|
|
161
194
|
state,
|
|
162
195
|
nonce: crypto.randomUUID(),
|
|
163
196
|
});
|
|
197
|
+
addNonOverridingParams(params, this.#extraAuthorizeParams);
|
|
164
198
|
return {
|
|
165
199
|
url: `${this.#discovery.authorizationEndpoint}?${params.toString()}`,
|
|
166
200
|
instructions:
|
|
@@ -188,23 +222,25 @@ export class XaiOAuthFlow extends OAuthCallbackFlow {
|
|
|
188
222
|
}
|
|
189
223
|
}
|
|
190
224
|
|
|
191
|
-
export async function loginXai(ctrl: OAuthController): Promise<OAuthCredentials> {
|
|
192
|
-
return new XaiOAuthFlow(ctrl).login();
|
|
225
|
+
export async function loginXai(ctrl: OAuthController, options?: XaiOAuthFlowOptions): Promise<OAuthCredentials> {
|
|
226
|
+
return new XaiOAuthFlow(ctrl, options).login();
|
|
193
227
|
}
|
|
194
228
|
|
|
195
|
-
export async function refreshXaiToken(
|
|
229
|
+
export async function refreshXaiToken(
|
|
230
|
+
refreshToken: string,
|
|
231
|
+
options?: AbortSignal | XaiOAuthRefreshOptions,
|
|
232
|
+
): Promise<OAuthCredentials> {
|
|
196
233
|
if (!refreshToken) {
|
|
197
234
|
throw new Error("xAI credentials are expired and do not include a refresh token");
|
|
198
235
|
}
|
|
236
|
+
const { signal, extraTokenParams = {} } = resolveRefreshOptions(options);
|
|
199
237
|
const discovery = await discoverXaiOAuthEndpoints(signal);
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
signal,
|
|
208
|
-
);
|
|
238
|
+
const body = {
|
|
239
|
+
grant_type: "refresh_token",
|
|
240
|
+
client_id: XAI_OAUTH_CLIENT_ID,
|
|
241
|
+
refresh_token: refreshToken,
|
|
242
|
+
};
|
|
243
|
+
addNonOverridingParams(body, extraTokenParams);
|
|
244
|
+
const tokenPayload = await postXaiToken(discovery.tokenEndpoint, body, signal);
|
|
209
245
|
return credentialsFromTokenPayload(tokenPayload, refreshToken);
|
|
210
246
|
}
|