@opengeni/xai-subscription 0.1.0
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/LICENSE +190 -0
- package/README.md +20 -0
- package/dist/bounded-operation.d.ts +8 -0
- package/dist/chunk-JEFGIUGN.js +80 -0
- package/dist/chunk-JEFGIUGN.js.map +1 -0
- package/dist/constants.d.ts +32 -0
- package/dist/constants.js +69 -0
- package/dist/constants.js.map +1 -0
- package/dist/errors.d.ts +12 -0
- package/dist/fetch.d.ts +7 -0
- package/dist/images.d.ts +22 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1245 -0
- package/dist/index.js.map +1 -0
- package/dist/models.d.ts +18 -0
- package/dist/normalize.d.ts +6 -0
- package/dist/oauth.d.ts +55 -0
- package/dist/proxy.d.ts +17 -0
- package/dist/quota.d.ts +24 -0
- package/dist/request-context.d.ts +26 -0
- package/dist/video.d.ts +53 -0
- package/package.json +45 -0
- package/src/bounded-operation.ts +33 -0
- package/src/constants.ts +53 -0
- package/src/errors.ts +34 -0
- package/src/fetch.ts +234 -0
- package/src/images.ts +176 -0
- package/src/index.ts +12 -0
- package/src/models.ts +99 -0
- package/src/normalize.ts +146 -0
- package/src/oauth.ts +335 -0
- package/src/proxy.ts +86 -0
- package/src/quota.ts +101 -0
- package/src/request-context.ts +31 -0
- package/src/video.ts +388 -0
package/src/fetch.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { XAI_CLIENT_MODE, XAI_CLIENT_VERSION, XAI_TOKEN_AUTH_HEADER_VALUE } from "./constants";
|
|
4
|
+
import { normalizeXaiResponseEventJson, normalizeXaiSubscriptionRequestBody } from "./normalize";
|
|
5
|
+
import { type XaiFinalContextUsage, xaiSubscriptionRequestStorage } from "./request-context";
|
|
6
|
+
|
|
7
|
+
export type XaiFetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
8
|
+
|
|
9
|
+
const REPLAYABLE_REQUEST_BODY_FACTORY = Symbol.for("opengeni.replayable-request-body-factory");
|
|
10
|
+
type ReplayableRequestInit = RequestInit & {
|
|
11
|
+
[REPLAYABLE_REQUEST_BODY_FACTORY]?: () => ReadableStream<Uint8Array>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const XAI_SUBSCRIPTION_TRANSPORT_ERROR_HEADER =
|
|
15
|
+
"x-opengeni-xai-subscription-transport-error";
|
|
16
|
+
export const XAI_SUBSCRIPTION_REQUEST_BODY_NORMALIZED_HEADER =
|
|
17
|
+
"x-opengeni-xai-subscription-body-normalized";
|
|
18
|
+
export const XAI_SUBSCRIPTION_REQUEST_MODEL_HEADER = "x-opengeni-xai-subscription-model";
|
|
19
|
+
export const XAI_SUBSCRIPTION_REQUEST_ID_HEADER = "x-opengeni-xai-subscription-request-id";
|
|
20
|
+
|
|
21
|
+
const MAX_ERROR_BODY_BYTES = 64 * 1024;
|
|
22
|
+
|
|
23
|
+
export function xaiSubscriptionFetch(base: XaiFetchLike): XaiFetchLike {
|
|
24
|
+
return async (input, init) => {
|
|
25
|
+
const context = xaiSubscriptionRequestStorage.getStore();
|
|
26
|
+
if (!context) throw new Error("SuperGrok subscription request context is unavailable");
|
|
27
|
+
|
|
28
|
+
const originalUrl = input instanceof Request ? input.url : String(input);
|
|
29
|
+
const url = new URL(originalUrl);
|
|
30
|
+
if (!url.pathname.endsWith("/responses")) {
|
|
31
|
+
throw new Error("SuperGrok subscription models require the Responses API");
|
|
32
|
+
}
|
|
33
|
+
const originalHeaders = new Headers(input instanceof Request ? input.headers : undefined);
|
|
34
|
+
if (init?.headers) {
|
|
35
|
+
new Headers(init.headers).forEach((value, key) => originalHeaders.set(key, value));
|
|
36
|
+
}
|
|
37
|
+
const normalized = originalHeaders.get(XAI_SUBSCRIPTION_REQUEST_BODY_NORMALIZED_HEADER) === "1";
|
|
38
|
+
const requestId = originalHeaders.get(XAI_SUBSCRIPTION_REQUEST_ID_HEADER) ?? randomUUID();
|
|
39
|
+
const handedOffModel = originalHeaders.get(XAI_SUBSCRIPTION_REQUEST_MODEL_HEADER);
|
|
40
|
+
originalHeaders.delete(XAI_SUBSCRIPTION_REQUEST_BODY_NORMALIZED_HEADER);
|
|
41
|
+
originalHeaders.delete(XAI_SUBSCRIPTION_REQUEST_ID_HEADER);
|
|
42
|
+
originalHeaders.delete(XAI_SUBSCRIPTION_REQUEST_MODEL_HEADER);
|
|
43
|
+
|
|
44
|
+
const replayableBodyFactory = (init as ReplayableRequestInit | undefined)?.[
|
|
45
|
+
REPLAYABLE_REQUEST_BODY_FACTORY
|
|
46
|
+
];
|
|
47
|
+
const body = await requestBodyText(input, init, replayableBodyFactory);
|
|
48
|
+
const parsed = body ? (JSON.parse(body) as unknown) : {};
|
|
49
|
+
const normalizedBody = normalized
|
|
50
|
+
? (parsed as Record<string, unknown>)
|
|
51
|
+
: normalizeXaiSubscriptionRequestBody(parsed, context.resolveModel, context.hostedSearch);
|
|
52
|
+
const model =
|
|
53
|
+
handedOffModel ??
|
|
54
|
+
(typeof normalizedBody.model === "string" ? normalizedBody.model : "grok-4.6");
|
|
55
|
+
|
|
56
|
+
const send = async (refresh: boolean): Promise<Response> => {
|
|
57
|
+
const token = refresh ? await context.refresh() : await context.getToken();
|
|
58
|
+
const headers = new Headers(originalHeaders);
|
|
59
|
+
headers.set("authorization", `Bearer ${token.accessToken}`);
|
|
60
|
+
headers.set("content-type", "application/json");
|
|
61
|
+
headers.set("accept", "text/event-stream");
|
|
62
|
+
headers.set("user-agent", `opengeni/${context.clientVersion || XAI_CLIENT_VERSION}`);
|
|
63
|
+
headers.set("x-grok-client-version", context.clientVersion || XAI_CLIENT_VERSION);
|
|
64
|
+
headers.set("x-grok-client-identifier", "opengeni");
|
|
65
|
+
headers.set("x-grok-client-mode", XAI_CLIENT_MODE);
|
|
66
|
+
headers.set("x-authenticateresponse", "authenticate-response");
|
|
67
|
+
headers.set("x-xai-token-auth", XAI_TOKEN_AUTH_HEADER_VALUE);
|
|
68
|
+
headers.set("x-userid", token.userId);
|
|
69
|
+
headers.set("x-grok-user-id", token.userId);
|
|
70
|
+
headers.set("x-grok-conv-id", context.sessionId);
|
|
71
|
+
headers.set("x-grok-session-id", context.sessionId);
|
|
72
|
+
headers.set("x-grok-req-id", requestId);
|
|
73
|
+
headers.set("x-grok-agent-id", context.turnId);
|
|
74
|
+
headers.set("x-grok-model-override", model);
|
|
75
|
+
return await base(url, {
|
|
76
|
+
...init,
|
|
77
|
+
method: init?.method ?? (input instanceof Request ? input.method : "POST"),
|
|
78
|
+
headers,
|
|
79
|
+
body: JSON.stringify(normalizedBody),
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
let response = await send(false);
|
|
84
|
+
if (response.status === 401) {
|
|
85
|
+
await response.body?.cancel().catch(() => undefined);
|
|
86
|
+
response = await send(true);
|
|
87
|
+
}
|
|
88
|
+
return await normalizeResponse(response, context.onFinalContextUsage);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function requestBodyText(
|
|
93
|
+
input: string | URL | Request,
|
|
94
|
+
init: RequestInit | undefined,
|
|
95
|
+
replayableBodyFactory: (() => ReadableStream<Uint8Array>) | undefined,
|
|
96
|
+
): Promise<string> {
|
|
97
|
+
if (typeof init?.body === "string") return init.body;
|
|
98
|
+
if (input instanceof Request) return await input.clone().text();
|
|
99
|
+
if (replayableBodyFactory) return await new Response(replayableBodyFactory()).text();
|
|
100
|
+
if (init?.body === undefined || init.body === null) return "";
|
|
101
|
+
throw new Error("SuperGrok subscription request body must be replayable JSON text");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function normalizeResponse(
|
|
105
|
+
response: Response,
|
|
106
|
+
onFinalContextUsage: ((usage: XaiFinalContextUsage) => void) | undefined,
|
|
107
|
+
): Promise<Response> {
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
110
|
+
const bounded =
|
|
111
|
+
bytes.byteLength > MAX_ERROR_BODY_BYTES ? bytes.slice(0, MAX_ERROR_BODY_BYTES) : bytes;
|
|
112
|
+
const headers = new Headers(response.headers);
|
|
113
|
+
headers.set(XAI_SUBSCRIPTION_TRANSPORT_ERROR_HEADER, "1");
|
|
114
|
+
return new Response(bounded, {
|
|
115
|
+
status: response.status,
|
|
116
|
+
statusText: response.statusText,
|
|
117
|
+
headers,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (!response.body) return response;
|
|
121
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
122
|
+
if (!contentType.includes("text/event-stream")) {
|
|
123
|
+
const value = await response.json();
|
|
124
|
+
const normalized = normalizeXaiResponseEventJson({
|
|
125
|
+
type: "response.completed",
|
|
126
|
+
response: value,
|
|
127
|
+
});
|
|
128
|
+
emitContextUsage(normalized.value, normalized.finalContextTokens, onFinalContextUsage);
|
|
129
|
+
const responseValue =
|
|
130
|
+
normalized.value && typeof normalized.value === "object" && !Array.isArray(normalized.value)
|
|
131
|
+
? (normalized.value as Record<string, unknown>).response
|
|
132
|
+
: value;
|
|
133
|
+
return Response.json(responseValue, {
|
|
134
|
+
status: response.status,
|
|
135
|
+
statusText: response.statusText,
|
|
136
|
+
headers: response.headers,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const reader = response.body.getReader();
|
|
140
|
+
const decoder = new TextDecoder();
|
|
141
|
+
const encoder = new TextEncoder();
|
|
142
|
+
let pending = "";
|
|
143
|
+
const body = new ReadableStream<Uint8Array>({
|
|
144
|
+
async pull(controller) {
|
|
145
|
+
const chunk = await reader.read();
|
|
146
|
+
pending += decoder.decode(chunk.value, { stream: !chunk.done });
|
|
147
|
+
const parts = pending.split("\n\n");
|
|
148
|
+
pending = parts.pop() ?? "";
|
|
149
|
+
for (const part of parts)
|
|
150
|
+
controller.enqueue(encoder.encode(`${normalizeSseEvent(part, onFinalContextUsage)}\n\n`));
|
|
151
|
+
if (chunk.done) {
|
|
152
|
+
if (pending)
|
|
153
|
+
controller.enqueue(encoder.encode(normalizeSseEvent(pending, onFinalContextUsage)));
|
|
154
|
+
controller.close();
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
cancel(reason) {
|
|
158
|
+
return reader.cancel(reason);
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
return new Response(body, {
|
|
162
|
+
status: response.status,
|
|
163
|
+
statusText: response.statusText,
|
|
164
|
+
headers: response.headers,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function normalizeSseEvent(
|
|
169
|
+
block: string,
|
|
170
|
+
onFinalContextUsage: ((usage: XaiFinalContextUsage) => void) | undefined,
|
|
171
|
+
): string {
|
|
172
|
+
const lines = block.split("\n");
|
|
173
|
+
return lines
|
|
174
|
+
.map((line) => {
|
|
175
|
+
if (!line.startsWith("data:")) return line;
|
|
176
|
+
const data = line.slice(5).trimStart();
|
|
177
|
+
if (!data || data === "[DONE]") return line;
|
|
178
|
+
try {
|
|
179
|
+
const normalized = normalizeXaiResponseEventJson(JSON.parse(data));
|
|
180
|
+
emitContextUsage(normalized.value, normalized.finalContextTokens, onFinalContextUsage);
|
|
181
|
+
return `data: ${JSON.stringify(normalized.value)}`;
|
|
182
|
+
} catch {
|
|
183
|
+
return line;
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
.join("\n");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function emitContextUsage(
|
|
190
|
+
value: unknown,
|
|
191
|
+
finalContextTokens: number | null,
|
|
192
|
+
sink: ((usage: XaiFinalContextUsage) => void) | undefined,
|
|
193
|
+
): void {
|
|
194
|
+
if (finalContextTokens === null || !sink) return;
|
|
195
|
+
const response =
|
|
196
|
+
value && typeof value === "object" && !Array.isArray(value)
|
|
197
|
+
? (value as Record<string, unknown>).response
|
|
198
|
+
: null;
|
|
199
|
+
const usage =
|
|
200
|
+
response && typeof response === "object" && !Array.isArray(response)
|
|
201
|
+
? (response as Record<string, unknown>).usage
|
|
202
|
+
: null;
|
|
203
|
+
const context =
|
|
204
|
+
usage && typeof usage === "object" && !Array.isArray(usage)
|
|
205
|
+
? (usage as Record<string, unknown>).context_details
|
|
206
|
+
: null;
|
|
207
|
+
if (!context || typeof context !== "object" || Array.isArray(context)) return;
|
|
208
|
+
const inputTokens = Number((context as Record<string, unknown>).input_tokens);
|
|
209
|
+
const outputTokens = Number((context as Record<string, unknown>).output_tokens);
|
|
210
|
+
if (!Number.isSafeInteger(inputTokens) || !Number.isSafeInteger(outputTokens)) return;
|
|
211
|
+
try {
|
|
212
|
+
sink({ inputTokens, outputTokens, totalTokens: finalContextTokens });
|
|
213
|
+
} catch {
|
|
214
|
+
// Usage observation must never alter model transport.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function isXaiSubscriptionTransportError(error: unknown): boolean {
|
|
219
|
+
let current: unknown = error;
|
|
220
|
+
for (let depth = 0; depth < 6 && current && typeof current === "object"; depth += 1) {
|
|
221
|
+
const value = current as Record<string, unknown>;
|
|
222
|
+
const headers = value.headers;
|
|
223
|
+
if (
|
|
224
|
+
headers &&
|
|
225
|
+
typeof headers === "object" &&
|
|
226
|
+
typeof (headers as { get?: unknown }).get === "function" &&
|
|
227
|
+
(headers as Headers).get(XAI_SUBSCRIPTION_TRANSPORT_ERROR_HEADER) === "1"
|
|
228
|
+
) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
current = value.cause;
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
234
|
+
}
|
package/src/images.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { pinnedFetch, readJsonBase64Field, readResponseTextBounded } from "@opengeni/network";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
XAI_CLIENT_VERSION,
|
|
5
|
+
XAI_IMAGE_MODEL,
|
|
6
|
+
XAI_IMAGE_REQUEST_TIMEOUT_MS,
|
|
7
|
+
XAI_PUBLIC_API_BASE_URL,
|
|
8
|
+
} from "./constants";
|
|
9
|
+
import { XaiSubscriptionError } from "./errors";
|
|
10
|
+
import type { XaiFetchLike } from "./fetch";
|
|
11
|
+
import type { XaiSubscriptionTokenSnapshot } from "./request-context";
|
|
12
|
+
|
|
13
|
+
const XAI_IMAGE_RESPONSE_MAX_BYTES = 90 * 1024 * 1024;
|
|
14
|
+
const XAI_IMAGE_MAX_BYTES = 64 * 1024 * 1024;
|
|
15
|
+
const XAI_IMAGE_ERROR_MAX_BYTES = 64 * 1024;
|
|
16
|
+
|
|
17
|
+
export type XaiImageReference = Readonly<{
|
|
18
|
+
mediaType: "image/png" | "image/jpeg" | "image/webp";
|
|
19
|
+
bytes: Uint8Array;
|
|
20
|
+
}>;
|
|
21
|
+
|
|
22
|
+
const defaultImageFetch: XaiFetchLike = async (input, init) =>
|
|
23
|
+
await pinnedFetch(
|
|
24
|
+
input,
|
|
25
|
+
init,
|
|
26
|
+
{ environment: "production", integrationsAllowPrivateNetworkTargets: false },
|
|
27
|
+
{ label: "xAI image generation", requireHttpsOutsideLocalTest: true },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export type XaiGeneratedImage = {
|
|
31
|
+
bytes: Uint8Array;
|
|
32
|
+
declaredMediaType: "image/png" | "image/jpeg" | "image/webp";
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export async function generateXaiSubscriptionImage(input: {
|
|
36
|
+
prompt: string;
|
|
37
|
+
aspectRatio?: string;
|
|
38
|
+
references?: readonly XaiImageReference[];
|
|
39
|
+
sessionId?: string;
|
|
40
|
+
getToken: () => Promise<XaiSubscriptionTokenSnapshot>;
|
|
41
|
+
refresh: () => Promise<XaiSubscriptionTokenSnapshot>;
|
|
42
|
+
abortSignal?: AbortSignal;
|
|
43
|
+
fetch?: XaiFetchLike;
|
|
44
|
+
requestTimeoutMs?: number;
|
|
45
|
+
baseUrl?: string;
|
|
46
|
+
}): Promise<XaiGeneratedImage> {
|
|
47
|
+
const timeoutMs = input.requestTimeoutMs ?? XAI_IMAGE_REQUEST_TIMEOUT_MS;
|
|
48
|
+
const deadline = new AbortController();
|
|
49
|
+
const timer = setTimeout(
|
|
50
|
+
() => deadline.abort(new XaiSubscriptionError("timeout", "xAI image generation timed out")),
|
|
51
|
+
timeoutMs,
|
|
52
|
+
);
|
|
53
|
+
const signal = input.abortSignal
|
|
54
|
+
? AbortSignal.any([input.abortSignal, deadline.signal])
|
|
55
|
+
: deadline.signal;
|
|
56
|
+
const fetchImpl = input.fetch ?? defaultImageFetch;
|
|
57
|
+
const references = input.references ?? [];
|
|
58
|
+
if (references.length > 3) {
|
|
59
|
+
throw new XaiSubscriptionError(
|
|
60
|
+
"provider_rejected",
|
|
61
|
+
"xAI image editing supports at most three references",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const url = `${(input.baseUrl ?? XAI_PUBLIC_API_BASE_URL).replace(/\/+$/, "")}/images/${references.length > 0 ? "edits" : "generations"}`;
|
|
65
|
+
const request = async (token: XaiSubscriptionTokenSnapshot): Promise<Response> =>
|
|
66
|
+
await fetchImpl(url, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
redirect: "error",
|
|
69
|
+
headers: {
|
|
70
|
+
accept: "application/json",
|
|
71
|
+
authorization: `Bearer ${token.accessToken}`,
|
|
72
|
+
"content-type": "application/json",
|
|
73
|
+
"user-agent": `opengeni/${XAI_CLIENT_VERSION}`,
|
|
74
|
+
"x-grok-client-version": XAI_CLIENT_VERSION,
|
|
75
|
+
"x-grok-client-identifier": "opengeni",
|
|
76
|
+
...(input.sessionId ? { "x-grok-session-id": input.sessionId } : {}),
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify(
|
|
79
|
+
references.length > 0
|
|
80
|
+
? {
|
|
81
|
+
model: XAI_IMAGE_MODEL,
|
|
82
|
+
prompt: input.prompt,
|
|
83
|
+
n: 1,
|
|
84
|
+
resolution: "1k",
|
|
85
|
+
response_format: "b64_json",
|
|
86
|
+
...(references.length === 1
|
|
87
|
+
? { image: referencePayload(references[0]!) }
|
|
88
|
+
: {
|
|
89
|
+
images: references.map(referencePayload),
|
|
90
|
+
aspect_ratio: input.aspectRatio ?? "auto",
|
|
91
|
+
}),
|
|
92
|
+
}
|
|
93
|
+
: {
|
|
94
|
+
model: XAI_IMAGE_MODEL,
|
|
95
|
+
prompt: input.prompt,
|
|
96
|
+
n: 1,
|
|
97
|
+
aspect_ratio: input.aspectRatio ?? "auto",
|
|
98
|
+
resolution: "1k",
|
|
99
|
+
response_format: "b64_json",
|
|
100
|
+
},
|
|
101
|
+
),
|
|
102
|
+
signal,
|
|
103
|
+
});
|
|
104
|
+
try {
|
|
105
|
+
let response = await request(await input.getToken());
|
|
106
|
+
if (response.status === 401) {
|
|
107
|
+
await response.body?.cancel().catch(() => undefined);
|
|
108
|
+
response = await request(await input.refresh());
|
|
109
|
+
}
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
const detail = await readResponseTextBounded(
|
|
112
|
+
response,
|
|
113
|
+
XAI_IMAGE_ERROR_MAX_BYTES,
|
|
114
|
+
"xAI image generation error",
|
|
115
|
+
{ signal },
|
|
116
|
+
).catch(() => "");
|
|
117
|
+
throw new XaiSubscriptionError(
|
|
118
|
+
"provider_rejected",
|
|
119
|
+
`xAI image generation failed (${response.status})${detail ? `: ${boundedMessage(detail)}` : ""}`,
|
|
120
|
+
response.status,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const bytes = await readJsonBase64Field(response, {
|
|
124
|
+
fieldName: "b64_json",
|
|
125
|
+
shape: "string",
|
|
126
|
+
maxResponseBytes: XAI_IMAGE_RESPONSE_MAX_BYTES,
|
|
127
|
+
maxDecodedBytes: XAI_IMAGE_MAX_BYTES,
|
|
128
|
+
label: "xAI image generation",
|
|
129
|
+
signal,
|
|
130
|
+
});
|
|
131
|
+
return { bytes, declaredMediaType: detectImageMediaType(bytes) };
|
|
132
|
+
} finally {
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function referencePayload(reference: XaiImageReference): { url: string } {
|
|
138
|
+
return {
|
|
139
|
+
url: `data:${reference.mediaType};base64,${Buffer.from(reference.bytes).toString("base64")}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function detectImageMediaType(bytes: Uint8Array): XaiGeneratedImage["declaredMediaType"] {
|
|
144
|
+
if (
|
|
145
|
+
bytes.byteLength >= 8 &&
|
|
146
|
+
[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every((byte, index) => bytes[index] === byte)
|
|
147
|
+
) {
|
|
148
|
+
return "image/png";
|
|
149
|
+
}
|
|
150
|
+
if (bytes.byteLength >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
|
|
151
|
+
return "image/jpeg";
|
|
152
|
+
}
|
|
153
|
+
if (
|
|
154
|
+
bytes.byteLength >= 12 &&
|
|
155
|
+
String.fromCharCode(...bytes.subarray(0, 4)) === "RIFF" &&
|
|
156
|
+
String.fromCharCode(...bytes.subarray(8, 12)) === "WEBP"
|
|
157
|
+
) {
|
|
158
|
+
return "image/webp";
|
|
159
|
+
}
|
|
160
|
+
throw new XaiSubscriptionError("invalid_response", "xAI returned an unsupported image format");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function boundedMessage(body: string): string {
|
|
164
|
+
try {
|
|
165
|
+
const value = JSON.parse(body) as Record<string, unknown>;
|
|
166
|
+
const error =
|
|
167
|
+
value.error && typeof value.error === "object" && !Array.isArray(value.error)
|
|
168
|
+
? (value.error as Record<string, unknown>)
|
|
169
|
+
: null;
|
|
170
|
+
const message = error?.message ?? value.message;
|
|
171
|
+
if (typeof message === "string") return message.replace(/\s+/g, " ").trim().slice(0, 1_000);
|
|
172
|
+
} catch {
|
|
173
|
+
// Preserve only a bounded normalized provider diagnostic.
|
|
174
|
+
}
|
|
175
|
+
return body.replace(/\s+/g, " ").trim().slice(0, 1_000);
|
|
176
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./constants";
|
|
2
|
+
export * from "./errors";
|
|
3
|
+
export * from "./bounded-operation";
|
|
4
|
+
export * from "./oauth";
|
|
5
|
+
export * from "./request-context";
|
|
6
|
+
export * from "./normalize";
|
|
7
|
+
export * from "./fetch";
|
|
8
|
+
export * from "./proxy";
|
|
9
|
+
export * from "./quota";
|
|
10
|
+
export * from "./models";
|
|
11
|
+
export * from "./images";
|
|
12
|
+
export * from "./video";
|
package/src/models.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
XAI_SUBSCRIPTION_AUTO_COMPACTION_PERCENT,
|
|
3
|
+
XAI_SUBSCRIPTION_EFFECTIVE_CONTEXT_PERCENT,
|
|
4
|
+
} from "./constants";
|
|
5
|
+
import type { XaiFetchLike } from "./fetch";
|
|
6
|
+
import { fetchXaiProxyJson, type XaiProxyAuthContext } from "./proxy";
|
|
7
|
+
|
|
8
|
+
export type XaiSubscriptionModelMetadata = {
|
|
9
|
+
slug: string;
|
|
10
|
+
name: string;
|
|
11
|
+
contextWindowTokens: number;
|
|
12
|
+
effectiveContextWindowTokens: number;
|
|
13
|
+
autoCompactTokenLimit: number;
|
|
14
|
+
maxCompletionTokens: number | null;
|
|
15
|
+
apiBackend: "responses" | "chat_completions" | "messages";
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export async function fetchXaiSubscriptionModels(input: {
|
|
19
|
+
context: XaiProxyAuthContext;
|
|
20
|
+
fetch?: XaiFetchLike;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
baseUrl?: string;
|
|
23
|
+
}): Promise<XaiSubscriptionModelMetadata[]> {
|
|
24
|
+
const body = await fetchXaiProxyJson<unknown>({
|
|
25
|
+
path: "models",
|
|
26
|
+
context: input.context,
|
|
27
|
+
...(input.fetch ? { fetch: input.fetch } : {}),
|
|
28
|
+
...(input.timeoutMs ? { timeoutMs: input.timeoutMs } : {}),
|
|
29
|
+
...(input.baseUrl ? { baseUrl: input.baseUrl } : {}),
|
|
30
|
+
maxBytes: 4 * 1024 * 1024,
|
|
31
|
+
label: "model metadata request",
|
|
32
|
+
});
|
|
33
|
+
const values = Array.isArray(body)
|
|
34
|
+
? body
|
|
35
|
+
: body && typeof body === "object" && Array.isArray((body as Record<string, unknown>).data)
|
|
36
|
+
? ((body as Record<string, unknown>).data as unknown[])
|
|
37
|
+
: [];
|
|
38
|
+
return values.flatMap((value) => {
|
|
39
|
+
const parsed = parseXaiSubscriptionModelMetadata(value);
|
|
40
|
+
return parsed ? [parsed] : [];
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseXaiSubscriptionModelMetadata(
|
|
45
|
+
value: unknown,
|
|
46
|
+
): XaiSubscriptionModelMetadata | null {
|
|
47
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
48
|
+
const object = value as Record<string, unknown>;
|
|
49
|
+
const meta =
|
|
50
|
+
object._meta && typeof object._meta === "object" && !Array.isArray(object._meta)
|
|
51
|
+
? (object._meta as Record<string, unknown>)
|
|
52
|
+
: {};
|
|
53
|
+
const slug = firstString(object.model, object.modelId, object.id, meta.model, meta.modelId);
|
|
54
|
+
const contextWindowTokens = firstPositiveInteger(
|
|
55
|
+
object.contextWindow,
|
|
56
|
+
object.context_window,
|
|
57
|
+
meta.contextWindow,
|
|
58
|
+
meta.totalContextTokens,
|
|
59
|
+
);
|
|
60
|
+
if (!slug || contextWindowTokens === null) return null;
|
|
61
|
+
const effectivePercent =
|
|
62
|
+
firstPositiveInteger(
|
|
63
|
+
object.effectiveContextWindowPercent,
|
|
64
|
+
object.effective_context_window_percent,
|
|
65
|
+
) ?? XAI_SUBSCRIPTION_EFFECTIVE_CONTEXT_PERCENT;
|
|
66
|
+
const autoCompactPercent =
|
|
67
|
+
firstPositiveInteger(
|
|
68
|
+
object.autoCompactThresholdPercent,
|
|
69
|
+
object.auto_compact_threshold_percent,
|
|
70
|
+
) ?? XAI_SUBSCRIPTION_AUTO_COMPACTION_PERCENT;
|
|
71
|
+
const backend = firstString(object.apiBackend, object.api_backend);
|
|
72
|
+
return {
|
|
73
|
+
slug,
|
|
74
|
+
name: firstString(object.name) ?? slug,
|
|
75
|
+
contextWindowTokens,
|
|
76
|
+
effectiveContextWindowTokens: Math.floor((contextWindowTokens * effectivePercent) / 100),
|
|
77
|
+
autoCompactTokenLimit: Math.floor((contextWindowTokens * autoCompactPercent) / 100),
|
|
78
|
+
maxCompletionTokens: firstPositiveInteger(
|
|
79
|
+
object.maxCompletionTokens,
|
|
80
|
+
object.max_completion_tokens,
|
|
81
|
+
),
|
|
82
|
+
apiBackend: backend === "chat_completions" || backend === "messages" ? backend : "responses",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function firstString(...values: unknown[]): string | null {
|
|
87
|
+
for (const value of values) {
|
|
88
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function firstPositiveInteger(...values: unknown[]): number | null {
|
|
94
|
+
for (const value of values) {
|
|
95
|
+
const number = Number(value);
|
|
96
|
+
if (Number.isSafeInteger(number) && number > 0) return number;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
package/src/normalize.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { XaiHostedSearchOptions } from "./request-context";
|
|
2
|
+
|
|
3
|
+
const XAI_INTERNAL_MODEL_HANDOFF_PREFIX = "supergrok/";
|
|
4
|
+
|
|
5
|
+
export function normalizeXaiSubscriptionRequestBody(
|
|
6
|
+
value: unknown,
|
|
7
|
+
resolveModel: (slug: string) => string,
|
|
8
|
+
hostedSearch?: XaiHostedSearchOptions,
|
|
9
|
+
): Record<string, unknown> {
|
|
10
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
11
|
+
throw new Error("SuperGrok subscription request body must be a JSON object");
|
|
12
|
+
}
|
|
13
|
+
const body = { ...(value as Record<string, unknown>) };
|
|
14
|
+
if (typeof body.model !== "string") {
|
|
15
|
+
throw new Error("SuperGrok subscription request is missing model");
|
|
16
|
+
}
|
|
17
|
+
const candidate = body.model.startsWith(XAI_INTERNAL_MODEL_HANDOFF_PREFIX)
|
|
18
|
+
? body.model.slice(XAI_INTERNAL_MODEL_HANDOFF_PREFIX.length)
|
|
19
|
+
: body.model;
|
|
20
|
+
body.model = resolveModel(candidate);
|
|
21
|
+
body.store = false;
|
|
22
|
+
|
|
23
|
+
const include = Array.isArray(body.include)
|
|
24
|
+
? body.include.filter((entry): entry is string => typeof entry === "string")
|
|
25
|
+
: [];
|
|
26
|
+
if (!include.includes("reasoning.encrypted_content")) {
|
|
27
|
+
include.push("reasoning.encrypted_content");
|
|
28
|
+
}
|
|
29
|
+
body.include = include;
|
|
30
|
+
|
|
31
|
+
const tools = Array.isArray(body.tools) ? body.tools.map(normalizeXaiSubscriptionTool) : [];
|
|
32
|
+
appendHostedTool(tools, "web_search", hostedSearch?.webSearch);
|
|
33
|
+
appendHostedTool(tools, "x_search", hostedSearch?.xSearch);
|
|
34
|
+
if (tools.length > 0) body.tools = tools;
|
|
35
|
+
return body;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeXaiSubscriptionTool(tool: unknown): unknown {
|
|
39
|
+
if (!tool || typeof tool !== "object" || Array.isArray(tool)) return tool;
|
|
40
|
+
const record = tool as Record<string, unknown>;
|
|
41
|
+
if (record.type !== "web_search") return tool;
|
|
42
|
+
// OpenAI's Responses tool factory adds this tuning hint. Grok's CLI proxy
|
|
43
|
+
// supports native web_search but rejects the OpenAI-only argument.
|
|
44
|
+
const { search_context_size: _unsupported, ...supported } = record;
|
|
45
|
+
return supported;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function appendHostedTool(
|
|
49
|
+
tools: unknown[],
|
|
50
|
+
type: "web_search" | "x_search",
|
|
51
|
+
options: boolean | Record<string, unknown> | undefined,
|
|
52
|
+
): void {
|
|
53
|
+
if (!options) return;
|
|
54
|
+
if (
|
|
55
|
+
tools.some(
|
|
56
|
+
(tool) =>
|
|
57
|
+
tool &&
|
|
58
|
+
typeof tool === "object" &&
|
|
59
|
+
!Array.isArray(tool) &&
|
|
60
|
+
(tool as Record<string, unknown>).type === type,
|
|
61
|
+
)
|
|
62
|
+
) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
tools.push(normalizeXaiSubscriptionTool(options === true ? { type } : { type, ...options }));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function normalizeXaiResponseEventJson(value: unknown): {
|
|
69
|
+
value: unknown;
|
|
70
|
+
finalContextTokens: number | null;
|
|
71
|
+
} {
|
|
72
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
73
|
+
return { value, finalContextTokens: null };
|
|
74
|
+
}
|
|
75
|
+
const event = value as Record<string, unknown>;
|
|
76
|
+
const response =
|
|
77
|
+
event.response && typeof event.response === "object" && !Array.isArray(event.response)
|
|
78
|
+
? { ...(event.response as Record<string, unknown>) }
|
|
79
|
+
: null;
|
|
80
|
+
if (!response) return { value, finalContextTokens: null };
|
|
81
|
+
|
|
82
|
+
let changed = false;
|
|
83
|
+
if (Array.isArray(response.tools)) {
|
|
84
|
+
const filtered = response.tools.filter((tool) => isOpenAiSdkResponseTool(tool));
|
|
85
|
+
if (filtered.length !== response.tools.length) {
|
|
86
|
+
response.tools = filtered;
|
|
87
|
+
changed = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let finalContextTokens: number | null = null;
|
|
92
|
+
if (event.type === "response.completed" || event.type === "response.incomplete") {
|
|
93
|
+
const usage =
|
|
94
|
+
response.usage && typeof response.usage === "object" && !Array.isArray(response.usage)
|
|
95
|
+
? { ...(response.usage as Record<string, unknown>) }
|
|
96
|
+
: null;
|
|
97
|
+
const context =
|
|
98
|
+
usage?.context_details &&
|
|
99
|
+
typeof usage.context_details === "object" &&
|
|
100
|
+
!Array.isArray(usage.context_details)
|
|
101
|
+
? (usage.context_details as Record<string, unknown>)
|
|
102
|
+
: null;
|
|
103
|
+
const inputTokens = finiteNonNegativeInteger(context?.input_tokens);
|
|
104
|
+
const outputTokens = finiteNonNegativeInteger(context?.output_tokens);
|
|
105
|
+
if (usage && inputTokens !== null && outputTokens !== null) {
|
|
106
|
+
finalContextTokens = inputTokens + outputTokens;
|
|
107
|
+
usage.total_tokens = finalContextTokens;
|
|
108
|
+
response.usage = usage;
|
|
109
|
+
changed = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
value: changed ? { ...event, response } : value,
|
|
114
|
+
finalContextTokens,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function finiteNonNegativeInteger(value: unknown): number | null {
|
|
119
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The OpenAI SDK knows these Responses tool declaration families. x_search is
|
|
123
|
+
// intentionally absent: xAI executes it server-side and echoes it only in the
|
|
124
|
+
// response.tools declaration list, while its output items remain untouched.
|
|
125
|
+
function isOpenAiSdkResponseTool(tool: unknown): boolean {
|
|
126
|
+
if (!tool || typeof tool !== "object" || Array.isArray(tool)) return false;
|
|
127
|
+
const type = (tool as Record<string, unknown>).type;
|
|
128
|
+
return (
|
|
129
|
+
typeof type === "string" &&
|
|
130
|
+
[
|
|
131
|
+
"function",
|
|
132
|
+
"file_search",
|
|
133
|
+
"computer_use_preview",
|
|
134
|
+
"computer",
|
|
135
|
+
"web_search",
|
|
136
|
+
"web_search_preview",
|
|
137
|
+
"code_interpreter",
|
|
138
|
+
"image_generation",
|
|
139
|
+
"local_shell",
|
|
140
|
+
"shell",
|
|
141
|
+
"apply_patch",
|
|
142
|
+
"mcp",
|
|
143
|
+
"custom",
|
|
144
|
+
].includes(type)
|
|
145
|
+
);
|
|
146
|
+
}
|