@gullabs/xai 0.9.0 → 0.16.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/dist/index.cjs CHANGED
@@ -3,6 +3,582 @@
3
3
  var core = require('@gullabs/core');
4
4
  var zod = require('zod');
5
5
 
6
+ // src/client.ts
7
+
8
+ // src/sse.ts
9
+ async function* readSseFrames(body, options = {}) {
10
+ const reader = body.getReader();
11
+ const decoder = new TextDecoder("utf-8");
12
+ let pieces = [];
13
+ let skipLf = false;
14
+ let event;
15
+ let data = [];
16
+ let first = true;
17
+ const field = (line) => {
18
+ if (line.startsWith(":")) return;
19
+ const colon = line.indexOf(":");
20
+ const name = colon === -1 ? line : line.slice(0, colon);
21
+ let value = colon === -1 ? "" : line.slice(colon + 1);
22
+ if (value.startsWith(" ")) value = value.slice(1);
23
+ if (name === "event") event = value;
24
+ else if (name === "data") data.push(value);
25
+ };
26
+ const abort = {
27
+ happened: false,
28
+ error: new Error("aborted")
29
+ };
30
+ let wake;
31
+ options.aborted?.catch((error) => {
32
+ abort.happened = true;
33
+ abort.error = error instanceof Error ? error : new Error(String(error));
34
+ wake?.(error);
35
+ });
36
+ const nextChunk = () => {
37
+ if (abort.happened) return Promise.reject(abort.error);
38
+ return new Promise((resolve, reject) => {
39
+ wake = reject;
40
+ reader.read().then(resolve, reject);
41
+ });
42
+ };
43
+ try {
44
+ for (; ; ) {
45
+ const result = await nextChunk();
46
+ if (!result.done) options.onChunk?.();
47
+ let text = result.done ? decoder.decode() : decoder.decode(result.value, { stream: true });
48
+ if (first && text.length > 0) {
49
+ text = text.replace(/^\uFEFF/, "");
50
+ first = false;
51
+ }
52
+ let start = 0;
53
+ if (skipLf && text.length > 0) {
54
+ skipLf = false;
55
+ if (text.charCodeAt(0) === 10) start = 1;
56
+ }
57
+ const lineEnd = /[\r\n]/g;
58
+ lineEnd.lastIndex = start;
59
+ for (let end = lineEnd.exec(text); end !== null; end = lineEnd.exec(text)) {
60
+ pieces.push(text.slice(start, end.index));
61
+ const line = pieces.length === 1 ? pieces[0] : pieces.join("");
62
+ pieces = [];
63
+ start = end.index + 1;
64
+ if (text.charCodeAt(end.index) === 13) {
65
+ if (start < text.length) {
66
+ if (text.charCodeAt(start) === 10) start += 1;
67
+ } else if (!result.done) {
68
+ skipLf = true;
69
+ }
70
+ }
71
+ lineEnd.lastIndex = start;
72
+ if (line !== "") {
73
+ field(line);
74
+ } else {
75
+ if (event !== void 0 || data.length > 0) {
76
+ yield { event, data: data.join("\n") };
77
+ }
78
+ event = void 0;
79
+ data = [];
80
+ }
81
+ }
82
+ if (start < text.length) pieces.push(text.slice(start));
83
+ if (result.done) return;
84
+ }
85
+ } finally {
86
+ wake = void 0;
87
+ reader.cancel().catch(() => void 0);
88
+ }
89
+ }
90
+
91
+ // src/stream.ts
92
+ var XaiStreamError = class extends Error {
93
+ failure;
94
+ progress;
95
+ terminalUsage;
96
+ servedServiceTier;
97
+ constructor(failure, context = {}) {
98
+ super(describeFailure(failure, context.cause), { cause: context.cause });
99
+ this.name = "XaiStreamError";
100
+ this.failure = failure;
101
+ this.progress = context.progress ?? { progressed: false, outputChars: 0 };
102
+ this.terminalUsage = context.terminalUsage;
103
+ this.servedServiceTier = context.serviceTier;
104
+ }
105
+ };
106
+ function describeFailure(failure, cause) {
107
+ switch (failure.kind) {
108
+ case "ended_early":
109
+ return `xAI stream ended before response.completed${failure.lastEventType !== void 0 ? ` (last event: ${failure.lastEventType})` : " (no events)"}`;
110
+ case "error_event":
111
+ return `xAI stream reported an error${failure.code !== void 0 ? ` (code "${failure.code}")` : ""}${failure.message !== void 0 ? `: ${failure.message}` : ""}`;
112
+ case "malformed":
113
+ return `xAI stream is malformed: ${failure.detail}`;
114
+ case "deadline":
115
+ return `the stream was still open after the ${failure.timeoutMs} ms request timeout`;
116
+ case "aborted":
117
+ return `xAI stream was stopped after output began${cause instanceof Error ? `: ${cause.message}` : ""}`;
118
+ case "idle":
119
+ return `the stream sent no bytes (heartbeats included) for ${failure.idleTimeoutMs} ms`;
120
+ case "cut":
121
+ return `xAI stream was cut after output began: ${cause instanceof Error ? cause.message : failure.detail}`;
122
+ case "not_event_stream":
123
+ return `xAI transport returned a non-event-stream body (content-type "${failure.contentType}"); transport.fetch must return the text/event-stream response of the request unchanged`;
124
+ }
125
+ }
126
+ function isRecord(value) {
127
+ return typeof value === "object" && value !== null && !Array.isArray(value);
128
+ }
129
+ function deepEqual(a, b) {
130
+ if (a === b) return true;
131
+ if (Array.isArray(a)) {
132
+ return Array.isArray(b) && a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
133
+ }
134
+ if (isRecord(a) && isRecord(b)) {
135
+ const keys = Object.keys(a);
136
+ return keys.length === Object.keys(b).length && keys.every((k) => k in b && deepEqual(a[k], b[k]));
137
+ }
138
+ return false;
139
+ }
140
+ function sameItemContent(a, b) {
141
+ const strip = (item) => {
142
+ const { id: _id, status: _status, ...rest } = item;
143
+ return rest;
144
+ };
145
+ return deepEqual(strip(a), strip(b));
146
+ }
147
+ function clone(value) {
148
+ return structuredClone(value);
149
+ }
150
+ var TERMINAL_EVENTS = /* @__PURE__ */ new Set([
151
+ "response.completed",
152
+ "response.incomplete",
153
+ "response.failed"
154
+ ]);
155
+ var MAX_STREAM_INDEX = 1e4;
156
+ function errorField(event, name) {
157
+ const nested = isRecord(event["error"]) ? event["error"] : void 0;
158
+ const value = event[name] ?? nested?.[name];
159
+ return typeof value === "string" ? value : void 0;
160
+ }
161
+ var CONSUMED_ITEM_TYPES = /* @__PURE__ */ new Set(["message", "reasoning", "function_call"]);
162
+ var OPENING_EVENTS = /* @__PURE__ */ new Set([
163
+ "response.created",
164
+ "response.in_progress",
165
+ "response.queued"
166
+ ]);
167
+ function usageOf(response) {
168
+ const usage = response?.["usage"];
169
+ if (isRecord(usage) && typeof usage["input_tokens"] === "number" && typeof usage["output_tokens"] === "number") {
170
+ return usage;
171
+ }
172
+ return void 0;
173
+ }
174
+ function itemOutputChars(item) {
175
+ let chars = 0;
176
+ const text = (value) => {
177
+ if (isRecord(value) && typeof value["text"] === "string")
178
+ chars += value["text"].length;
179
+ };
180
+ if (Array.isArray(item["content"])) item["content"].forEach(text);
181
+ if (Array.isArray(item["summary"])) item["summary"].forEach(text);
182
+ if (typeof item["arguments"] === "string") chars += item["arguments"].length;
183
+ if (typeof item["input"] === "string") chars += item["input"].length;
184
+ return chars;
185
+ }
186
+ var XaiStreamReducer = class {
187
+ items = /* @__PURE__ */ new Map();
188
+ snapshot;
189
+ terminal;
190
+ lastType;
191
+ progressed = false;
192
+ skipped = [];
193
+ /**
194
+ * Applies one SSE frame. A frame with no `data` (a bare `event: keepalive`) or
195
+ * the `[DONE]` sentinel carries nothing and is skipped. A body that is not JSON
196
+ * is a malformed stream; JSON that is not an event is skipped and reported.
197
+ * Returns `true` once the terminal event has been seen.
198
+ */
199
+ pushFrame(frame) {
200
+ if (frame.data === "" || frame.data === "[DONE]") return false;
201
+ let parsed;
202
+ try {
203
+ parsed = JSON.parse(frame.data);
204
+ } catch (cause) {
205
+ throw new XaiStreamError(
206
+ {
207
+ kind: "malformed",
208
+ detail: `an event body is not valid JSON (${cause instanceof Error ? cause.message : String(cause)})`
209
+ },
210
+ { ...this.context(), cause }
211
+ );
212
+ }
213
+ if (isRecord(parsed) && typeof parsed["type"] !== "string") {
214
+ if (frame.event === "error" || isRecord(parsed["error"])) {
215
+ return this.push({ ...parsed, type: "error" });
216
+ }
217
+ }
218
+ return this.push(parsed);
219
+ }
220
+ /** Applies one event. Returns `true` once the terminal event has been seen. */
221
+ push(event) {
222
+ if (!isRecord(event) || typeof event["type"] !== "string") {
223
+ this.skipped.push("an event that has no string `type`");
224
+ return false;
225
+ }
226
+ const type = event["type"];
227
+ this.lastType = type;
228
+ if (TERMINAL_EVENTS.has(type)) {
229
+ const response = event["response"];
230
+ if (!isRecord(response)) {
231
+ throw new XaiStreamError(
232
+ { kind: "malformed", detail: `${type} carries no response object` },
233
+ this.context()
234
+ );
235
+ }
236
+ this.terminal = { type, response };
237
+ return true;
238
+ }
239
+ if (!OPENING_EVENTS.has(type) && type !== "error" && type.startsWith("response.")) {
240
+ this.progressed = true;
241
+ }
242
+ switch (type) {
243
+ case "response.created":
244
+ case "response.in_progress":
245
+ if (isRecord(event["response"])) this.snapshot = event["response"];
246
+ return false;
247
+ case "error":
248
+ throw new XaiStreamError(
249
+ {
250
+ kind: "error_event",
251
+ code: errorField(event, "code"),
252
+ message: errorField(event, "message")
253
+ },
254
+ this.context()
255
+ );
256
+ case "response.output_item.added":
257
+ case "response.output_item.done": {
258
+ const rawIndex = event["output_index"];
259
+ const index = typeof rawIndex === "number" && Number.isInteger(rawIndex) && rawIndex >= 0 ? this.index(rawIndex, "output_index") : void 0;
260
+ const item = event["item"];
261
+ if (index === void 0 || !isRecord(item) || typeof item["type"] !== "string") {
262
+ this.skipped.push(`a ${type} event with no integer output_index or typed item`);
263
+ return false;
264
+ }
265
+ this.items.set(index, { item: clone(item), done: type.endsWith(".done") });
266
+ return false;
267
+ }
268
+ default:
269
+ this.applyDelta(type, event);
270
+ return false;
271
+ }
272
+ }
273
+ /**
274
+ * An event index, or `undefined` when the event has none or it is not a
275
+ * non-negative integer (the event is then skipped, noted). An index above
276
+ * {@link MAX_STREAM_INDEX} is a malformed stream, not a value to write.
277
+ */
278
+ index(value, name) {
279
+ if (typeof value !== "number") return void 0;
280
+ if (!Number.isInteger(value) || value < 0) {
281
+ this.skipped.push(`an event whose ${name} is not a non-negative integer`);
282
+ return void 0;
283
+ }
284
+ if (value > MAX_STREAM_INDEX) {
285
+ throw new XaiStreamError(
286
+ {
287
+ kind: "malformed",
288
+ detail: `${name} ${value} is above the ${MAX_STREAM_INDEX} this client accepts`
289
+ },
290
+ this.context()
291
+ );
292
+ }
293
+ return value;
294
+ }
295
+ /** The error for a stream that ended without a terminal event. */
296
+ endedEarly() {
297
+ return new XaiStreamError(
298
+ { kind: "ended_early", lastEventType: this.lastType },
299
+ this.context()
300
+ );
301
+ }
302
+ /** What the stream had delivered so far. */
303
+ progress() {
304
+ let outputChars = 0;
305
+ for (const entry of this.items.values()) outputChars += itemOutputChars(entry.item);
306
+ return { progressed: this.progressed || this.terminal !== void 0, outputChars };
307
+ }
308
+ /** The progress, the terminal usage when one arrived, and the tier, for a failure. */
309
+ context() {
310
+ const terminalUsage = usageOf(this.terminal?.response);
311
+ const tier = (this.terminal?.response ?? this.snapshot)?.["service_tier"];
312
+ return {
313
+ progress: this.progress(),
314
+ ...terminalUsage !== void 0 ? { terminalUsage } : {},
315
+ ...typeof tier === "string" && tier.length > 0 ? { serviceTier: tier } : {}
316
+ };
317
+ }
318
+ /**
319
+ * The terminal response with its output reconciled against the events. Never
320
+ * fails once a terminal event with a response arrived.
321
+ *
322
+ * @throws XaiStreamError only when the stream has no terminal event yet.
323
+ */
324
+ result() {
325
+ const terminal = this.terminal;
326
+ if (terminal === void 0) throw this.endedEarly();
327
+ const response = terminal.response;
328
+ if (terminal.type === "response.failed") {
329
+ const status2 = response["status"];
330
+ return {
331
+ response: {
332
+ ...response,
333
+ status: status2 === "failed" || status2 === "cancelled" ? status2 : "failed"
334
+ },
335
+ notes: [],
336
+ outputBegan: this.progressed
337
+ };
338
+ }
339
+ const status = terminal.type === "response.incomplete" ? "incomplete" : response["status"];
340
+ const { output, notes } = this.reconcile(response["output"], status);
341
+ return {
342
+ response: { ...response, status, output },
343
+ notes: [...this.skippedNotes(), ...notes],
344
+ outputBegan: this.progressed
345
+ };
346
+ }
347
+ skippedNotes() {
348
+ if (this.skipped.length === 0) return [];
349
+ const counts = /* @__PURE__ */ new Map();
350
+ for (const what of this.skipped) counts.set(what, (counts.get(what) ?? 0) + 1);
351
+ return [...counts].map(
352
+ ([what, n]) => `xai: skipped ${n} stream event(s) that could not be used (${what}); the final response was used for them.`
353
+ );
354
+ }
355
+ /**
356
+ * Deltas build up the item an `added` event opened. A delta for an item the
357
+ * stream never opened is ignored: the terminal object or a `done` event still
358
+ * carries the item, and a lost delta must not fail a billed call.
359
+ */
360
+ applyDelta(type, event) {
361
+ const entry = this.items.get(
362
+ typeof event["output_index"] === "number" ? event["output_index"] : -1
363
+ );
364
+ if (entry === void 0) return;
365
+ const item = entry.item;
366
+ const contentIndex = this.index(event["content_index"], "content_index");
367
+ const summaryIndex = this.index(event["summary_index"], "summary_index");
368
+ const textAt = (list, index, defaults) => {
369
+ if (!Array.isArray(list) || typeof index !== "number") return {};
370
+ const existing = list[index];
371
+ if (isRecord(existing)) return existing;
372
+ const created = { ...defaults };
373
+ list[index] = created;
374
+ return created;
375
+ };
376
+ const content = () => {
377
+ if (!Array.isArray(item["content"])) item["content"] = [];
378
+ return item["content"];
379
+ };
380
+ const summary = () => {
381
+ if (!Array.isArray(item["summary"])) item["summary"] = [];
382
+ return item["summary"];
383
+ };
384
+ const append = (target, key, delta) => {
385
+ if (typeof delta !== "string") return;
386
+ target[key] = `${typeof target[key] === "string" ? target[key] : ""}${delta}`;
387
+ };
388
+ switch (type) {
389
+ case "response.content_part.added":
390
+ case "response.content_part.done":
391
+ if (typeof contentIndex === "number" && isRecord(event["part"])) {
392
+ content()[contentIndex] = clone(event["part"]);
393
+ }
394
+ return;
395
+ case "response.output_text.delta":
396
+ append(
397
+ textAt(content(), contentIndex, { type: "output_text", text: "" }),
398
+ "text",
399
+ event["delta"]
400
+ );
401
+ return;
402
+ case "response.output_text.done": {
403
+ const part = textAt(content(), contentIndex, { type: "output_text", text: "" });
404
+ if (typeof event["text"] === "string") part["text"] = event["text"];
405
+ return;
406
+ }
407
+ case "response.output_text.annotation.added": {
408
+ const part = textAt(content(), contentIndex, { type: "output_text", text: "" });
409
+ if (!Array.isArray(part["annotations"])) part["annotations"] = [];
410
+ const index = this.index(event["annotation_index"], "annotation_index");
411
+ if (index !== void 0 && event["annotation"] !== void 0) {
412
+ part["annotations"][index] = clone(event["annotation"]);
413
+ }
414
+ return;
415
+ }
416
+ case "response.reasoning_summary_part.added":
417
+ case "response.reasoning_summary_part.done":
418
+ if (typeof summaryIndex === "number" && isRecord(event["part"])) {
419
+ summary()[summaryIndex] = clone(event["part"]);
420
+ }
421
+ return;
422
+ case "response.reasoning_summary_text.delta":
423
+ append(
424
+ textAt(summary(), summaryIndex, { type: "summary_text", text: "" }),
425
+ "text",
426
+ event["delta"]
427
+ );
428
+ return;
429
+ case "response.reasoning_summary_text.done": {
430
+ const part = textAt(summary(), summaryIndex, { type: "summary_text", text: "" });
431
+ if (typeof event["text"] === "string") part["text"] = event["text"];
432
+ return;
433
+ }
434
+ case "response.function_call_arguments.delta":
435
+ append(item, "arguments", event["delta"]);
436
+ return;
437
+ case "response.function_call_arguments.done":
438
+ if (typeof event["arguments"] === "string") item["arguments"] = event["arguments"];
439
+ return;
440
+ // xAI streams the input of a server tool call (x_search) as a custom tool call.
441
+ case "response.custom_tool_call_input.delta":
442
+ append(item, "input", event["delta"]);
443
+ return;
444
+ case "response.custom_tool_call_input.done":
445
+ if (typeof event["input"] === "string") item["input"] = event["input"];
446
+ return;
447
+ default:
448
+ return;
449
+ }
450
+ }
451
+ /** The items the events built, in output order, finished for a response that ended. */
452
+ eventItems(responseStatus) {
453
+ return [...this.items.entries()].sort(([a], [b]) => a - b).map(([, entry]) => {
454
+ const item = clone(entry.item);
455
+ if (!entry.done) item["status"] = finishedStatus(responseStatus);
456
+ return item;
457
+ });
458
+ }
459
+ reconcile(finalOutput, responseStatus) {
460
+ if (finalOutput !== void 0 && !Array.isArray(finalOutput)) {
461
+ return {
462
+ output: this.eventItems(responseStatus),
463
+ notes: [
464
+ "xai: the final response `output` is not an array; the output items were built from the stream events."
465
+ ]
466
+ };
467
+ }
468
+ const finalItems = [];
469
+ let dropped = 0;
470
+ for (const item of finalOutput ?? []) {
471
+ if (isRecord(item)) finalItems.push(clone(item));
472
+ else dropped++;
473
+ }
474
+ const notes = [];
475
+ if (dropped > 0) {
476
+ notes.push(
477
+ `xai: dropped ${dropped} non-object item(s) from the final response \`output\`.`
478
+ );
479
+ }
480
+ const groupOf = (item) => {
481
+ const id = item["id"];
482
+ return typeof id === "string" && id.length > 0 ? `id:${id}` : `anon:${String(item["type"])}`;
483
+ };
484
+ const labelOf = (item) => typeof item["id"] === "string" && item["id"].length > 0 ? `${String(item["type"])} ${item["id"]}` : String(item["type"]);
485
+ const finalGroups = /* @__PURE__ */ new Map();
486
+ for (const item of finalItems) {
487
+ const group = groupOf(item);
488
+ finalGroups.set(group, [...finalGroups.get(group) ?? [], item]);
489
+ }
490
+ const eventGroups = /* @__PURE__ */ new Map();
491
+ for (const entry of [...this.items.entries()].sort(([a], [b]) => a - b)) {
492
+ const group = groupOf(entry[1].item);
493
+ eventGroups.set(group, [...eventGroups.get(group) ?? [], entry]);
494
+ }
495
+ const bound = /* @__PURE__ */ new Set();
496
+ const unbound = [];
497
+ for (const [group, entries] of eventGroups) {
498
+ const finals = finalGroups.get(group) ?? [];
499
+ const endOffset = finals.length - entries.length;
500
+ const matches = (offset2) => entries.filter(([, streamed], i) => {
501
+ const finalItem = finals[i + offset2];
502
+ return finalItem !== void 0 && sameItemContent(finalItem, streamed.item);
503
+ }).length;
504
+ const offset = matches(0) > matches(endOffset) ? 0 : endOffset;
505
+ entries.forEach((entry, i) => {
506
+ const finalItem = finals[i + offset];
507
+ if (finalItem === void 0) {
508
+ unbound.push(entry);
509
+ return;
510
+ }
511
+ bound.add(finalItem);
512
+ const [, streamed] = entry;
513
+ if (finalItem["type"] !== streamed.item["type"]) {
514
+ notes.push(
515
+ `xai: output item ${labelOf(finalItem)} is a "${String(streamed.item["type"])}" in the stream and a "${String(finalItem["type"])}" in the final response; the final response is used.`
516
+ );
517
+ return;
518
+ }
519
+ if (!streamed.done) return;
520
+ const filled = [];
521
+ const diverged = [];
522
+ for (const [field, value] of Object.entries(streamed.item)) {
523
+ if (!(field in finalItem)) {
524
+ finalItem[field] = clone(value);
525
+ filled.push(field);
526
+ } else if (!deepEqual(finalItem[field], value)) {
527
+ diverged.push(field);
528
+ }
529
+ }
530
+ if (filled.length > 0) {
531
+ notes.push(
532
+ `xai: output item ${labelOf(finalItem)} lacked field(s) [${filled.join(", ")}] in the final response; taken from the stream.`
533
+ );
534
+ }
535
+ if (diverged.length > 0 && CONSUMED_ITEM_TYPES.has(String(finalItem["type"]))) {
536
+ notes.push(
537
+ `xai: output item ${labelOf(finalItem)} differs between the stream and the final response in field(s) [${diverged.join(", ")}]; the final response is used.`
538
+ );
539
+ }
540
+ });
541
+ }
542
+ const spare = finalItems.filter((item) => !bound.has(item));
543
+ const insertions = [];
544
+ const rebuilt = [];
545
+ const assembled = [];
546
+ for (const [index, entry] of unbound.sort(([a], [b]) => a - b)) {
547
+ const twin = spare.findIndex((item2) => sameItemContent(item2, entry.item));
548
+ if (twin !== -1) {
549
+ spare.splice(twin, 1);
550
+ continue;
551
+ }
552
+ const item = clone(entry.item);
553
+ if (!entry.done) {
554
+ item["status"] = finishedStatus(responseStatus);
555
+ assembled.push(labelOf(item));
556
+ } else {
557
+ rebuilt.push(labelOf(item));
558
+ }
559
+ insertions.push({ index, item });
560
+ }
561
+ const output = [...finalItems];
562
+ for (const { index, item } of insertions) {
563
+ output.splice(Math.min(index, output.length), 0, item);
564
+ }
565
+ if (rebuilt.length > 0) {
566
+ notes.push(
567
+ `xai: the final response lacked ${rebuilt.length} output item(s) [${rebuilt.join(", ")}] that the stream completed; they were rebuilt from the stream events.`
568
+ );
569
+ }
570
+ if (assembled.length > 0) {
571
+ notes.push(
572
+ `xai: the final response lacked ${assembled.length} output item(s) [${assembled.join(", ")}] that the stream never completed; they were assembled from the stream deltas and may lack provider-only fields.`
573
+ );
574
+ }
575
+ return { output, notes };
576
+ }
577
+ };
578
+ function finishedStatus(responseStatus) {
579
+ return responseStatus === "incomplete" ? "incomplete" : "completed";
580
+ }
581
+
6
582
  // src/client.ts
7
583
  function requireApiKey(auth) {
8
584
  if (!("apiKey" in auth) || typeof auth.apiKey !== "string" || auth.apiKey.trim() === "") {
@@ -14,22 +590,227 @@ function requireApiKey(auth) {
14
590
  }
15
591
  return auth.apiKey;
16
592
  }
17
- async function buildXaiClient(auth) {
593
+ function isRemainingQuotaHeader(name) {
594
+ return name.startsWith("x-ratelimit-remaining");
595
+ }
596
+ function readXaiResponseMeta(headers, sdkRequestId) {
597
+ const requestId = headers.get("x-request-id") ?? void 0;
598
+ const remaining = {};
599
+ headers.forEach((value, name) => {
600
+ const lower = name.toLowerCase();
601
+ if (isRemainingQuotaHeader(lower)) remaining[lower] = value;
602
+ });
603
+ return {
604
+ ...requestId !== void 0 && requestId !== "" ? { requestId } : {},
605
+ ...Object.keys(remaining).length > 0 ? { rateLimitRemaining: remaining } : {}
606
+ };
607
+ }
608
+ var XAI_RESERVED_FETCH_OPTION_KEYS = [
609
+ "headers",
610
+ "signal",
611
+ "body",
612
+ "method"
613
+ ];
614
+ var XAI_DEFAULT_TIMEOUT_MS = 36e5;
615
+ var XAI_TIMEOUT_BUFFER_MS = 5e3;
616
+ var XAI_MAX_TIMEOUT_MS = 2147483647 - XAI_TIMEOUT_BUFFER_MS;
617
+ async function buildXaiClient(auth, transport) {
18
618
  const apiKey = requireApiKey(auth);
19
619
  const { default: OpenAI } = await import('openai');
20
620
  const client = new OpenAI({
21
621
  apiKey,
22
622
  baseURL: "https://api.x.ai/v1",
23
- maxRetries: 0
623
+ maxRetries: 0,
624
+ ...transport !== void 0 ? {
625
+ fetch: transport.fetch,
626
+ ...transport.fetchOptions !== void 0 ? { fetchOptions: transport.fetchOptions } : {}
627
+ } : {}
24
628
  });
629
+ const idleTimeoutMs = transport?.idleTimeoutMs;
25
630
  return {
26
631
  responses: {
27
632
  async create(params, options) {
28
- return client.responses.create(params, options);
633
+ const { onResponse, timeout, signal } = options ?? {};
634
+ const send = (p, o) => client.responses.create(
635
+ p,
636
+ o
637
+ );
638
+ const controller = new AbortController();
639
+ const forwardAbort = () => {
640
+ controller.abort(signal?.reason);
641
+ };
642
+ if (signal?.aborted === true) forwardAbort();
643
+ else signal?.addEventListener("abort", forwardAbort, { once: true });
644
+ const startedAt = performance.now();
645
+ let deadlineTimer;
646
+ let idleTimer;
647
+ const ended = { deadline: false, idle: false };
648
+ const reducer = new XaiStreamReducer();
649
+ const failure = () => {
650
+ if (ended.deadline) {
651
+ return new XaiStreamError(
652
+ { kind: "deadline", timeoutMs: timeout ?? 0 },
653
+ reducer.context()
654
+ );
655
+ }
656
+ if (ended.idle) {
657
+ return new XaiStreamError(
658
+ { kind: "idle", idleTimeoutMs: idleTimeoutMs ?? 0 },
659
+ reducer.context()
660
+ );
661
+ }
662
+ return void 0;
663
+ };
664
+ const stopped = () => {
665
+ const ours = failure();
666
+ if (ours !== void 0) return ours;
667
+ if (signal?.aborted === true) return abortFailure(signal, reducer);
668
+ return void 0;
669
+ };
670
+ try {
671
+ const response = await send(
672
+ { ...params, stream: true },
673
+ {
674
+ signal: controller.signal,
675
+ headers: { accept: "text/event-stream" },
676
+ ...timeout !== void 0 ? { timeout } : {}
677
+ }
678
+ ).asResponse();
679
+ if (timeout !== void 0) {
680
+ deadlineTimer = setTimeout(
681
+ () => {
682
+ ended.deadline = true;
683
+ controller.abort();
684
+ },
685
+ Math.max(0, timeout - (performance.now() - startedAt))
686
+ );
687
+ }
688
+ const touch = () => {
689
+ if (idleTimeoutMs === void 0) return;
690
+ if (idleTimer !== void 0) clearTimeout(idleTimer);
691
+ idleTimer = setTimeout(() => {
692
+ ended.idle = true;
693
+ controller.abort();
694
+ }, idleTimeoutMs);
695
+ };
696
+ touch();
697
+ const aborted = new Promise((_resolve, reject) => {
698
+ const onAbort = () => {
699
+ reject(new Error("aborted"));
700
+ };
701
+ if (controller.signal.aborted) onAbort();
702
+ else controller.signal.addEventListener("abort", onAbort, { once: true });
703
+ });
704
+ aborted.catch(() => void 0);
705
+ const contentType = response.headers.get("content-type");
706
+ if (contentType !== null && !/text\/event-stream/i.test(contentType)) {
707
+ const bodySnippet = await readBodySnippet(response.body, aborted);
708
+ const why = stopped();
709
+ if (why instanceof Error) throw why;
710
+ throw new XaiStreamError(
711
+ {
712
+ kind: "not_event_stream",
713
+ contentType,
714
+ ...bodySnippet !== void 0 ? { bodySnippet } : {}
715
+ },
716
+ {
717
+ cause: {
718
+ contentType,
719
+ ...bodySnippet !== void 0 ? { bodySnippet } : {}
720
+ }
721
+ }
722
+ );
723
+ }
724
+ if (response.body === null) throw reducer.endedEarly();
725
+ let terminal = false;
726
+ try {
727
+ for await (const frame of readSseFrames(response.body, {
728
+ onChunk: touch,
729
+ aborted
730
+ })) {
731
+ if (reducer.pushFrame(frame)) {
732
+ terminal = true;
733
+ break;
734
+ }
735
+ }
736
+ } catch (err) {
737
+ const why = stopped() ?? streamFailure(err, reducer);
738
+ throw why;
739
+ }
740
+ if (!terminal) {
741
+ const why = stopped() ?? reducer.endedEarly();
742
+ throw why;
743
+ }
744
+ const { response: reduced, notes, outputBegan } = reducer.result();
745
+ onResponse?.({
746
+ ...readXaiResponseMeta(response.headers),
747
+ ...notes.length > 0 ? { streamNotes: notes } : {},
748
+ streamProgressed: outputBegan
749
+ });
750
+ return reduced;
751
+ } finally {
752
+ if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
753
+ if (idleTimer !== void 0) clearTimeout(idleTimer);
754
+ signal?.removeEventListener("abort", forwardAbort);
755
+ }
29
756
  }
30
757
  }
31
758
  };
32
759
  }
760
+ function abortError(signal) {
761
+ const reason = signal.reason;
762
+ if (reason instanceof core.LlmError) return reason;
763
+ return Object.assign(new Error("Request was aborted."), {
764
+ name: "AbortError",
765
+ cause: reason
766
+ });
767
+ }
768
+ function abortFailure(signal, reducer) {
769
+ if (!reducer.progress().progressed) return abortError(signal);
770
+ return new XaiStreamError(
771
+ { kind: "aborted" },
772
+ { ...reducer.context(), cause: signal.reason }
773
+ );
774
+ }
775
+ var BODY_SNIPPET_CHARS = 500;
776
+ async function readBodySnippet(body, aborted) {
777
+ if (body === null) return void 0;
778
+ const reader = body.getReader();
779
+ const decoder = new TextDecoder("utf-8");
780
+ let text = "";
781
+ try {
782
+ while (text.length < 2 * BODY_SNIPPET_CHARS) {
783
+ const chunk = await Promise.race([reader.read(), aborted]);
784
+ if (chunk.done) break;
785
+ text += decoder.decode(chunk.value, { stream: true });
786
+ }
787
+ } catch {
788
+ } finally {
789
+ reader.cancel().catch(() => void 0);
790
+ }
791
+ const snippet = core.redactSecrets(text).slice(0, BODY_SNIPPET_CHARS);
792
+ return snippet.length > 0 ? snippet : void 0;
793
+ }
794
+ function streamFailure(err, reducer) {
795
+ if (err instanceof XaiStreamError) return err;
796
+ if (!reducer.progress().progressed) return err;
797
+ return new XaiStreamError(
798
+ { kind: "cut", detail: err instanceof Error ? err.message : String(err) },
799
+ { ...reducer.context(), cause: err }
800
+ );
801
+ }
802
+
803
+ // src/model-limits.ts
804
+ var grok4Limits = () => Object.freeze({ contextWindow: 5e5, maxOutputTokens: null });
805
+ var XAI_MODEL_LIMITS = {
806
+ "grok-4.5": grok4Limits(),
807
+ "grok-4.6": grok4Limits(),
808
+ "grok-4.7": grok4Limits()
809
+ };
810
+ var XAI_INPUT_MIME_TYPES = Object.freeze([
811
+ "image/jpeg",
812
+ "image/png"
813
+ ]);
33
814
  var webSearchFlags = {
34
815
  enableImageUnderstanding: zod.z.boolean().optional().meta({
35
816
  title: "Enable Image Understanding",
@@ -105,6 +886,21 @@ var XaiToolsSchema = zod.z.union([
105
886
  title: "XaiTools",
106
887
  description: "xAI Live Search tools. At most one web_search and at most one x_search."
107
888
  });
889
+ var maxWebSearchCalls = zod.z.number().int().min(1).meta({
890
+ title: "Max Web Search Calls",
891
+ description: "Ceiling on web_search calls. Requires a web_search tool. Compared with xAI's reported counter after the call."
892
+ });
893
+ var maxXItems = zod.z.number().int().min(1).meta({
894
+ title: "Max X Items",
895
+ description: "Ceiling on X posts plus users fetched. Requires an x_search tool. Compared with xAI's reported counters after the call."
896
+ });
897
+ var XaiSearchBudgetSchema = zod.z.union([
898
+ zod.z.strictObject({ maxWebSearchCalls, maxXItems: maxXItems.optional() }),
899
+ zod.z.strictObject({ maxWebSearchCalls: maxWebSearchCalls.optional(), maxXItems })
900
+ ]).meta({
901
+ title: "Search Budget",
902
+ description: "Observed after the call, never sent to xAI (which has no per-call search ceiling). Over budget: a warning and usage.details.search_budget_exceeded = 1; the already-billed result is still returned. Needs at least one ceiling, and the tool each ceiling counts (maxWebSearchCalls: web_search, maxXItems: x_search) in tools."
903
+ });
108
904
  var XaiProviderOptionsSchema = zod.z.strictObject({
109
905
  promptCacheKey: zod.z.string().min(1).optional().meta({
110
906
  title: "Prompt Cache Key",
@@ -125,7 +921,23 @@ var XaiProviderOptionsSchema = zod.z.strictObject({
125
921
  maxTurns: zod.z.number().int().min(1).optional().meta({
126
922
  title: "Max Turns",
127
923
  description: "xAI Responses `max_turns`: cap on agentic tool-calling turns for the server-side search tools. Requires `tools`. A turn can run several searches. Not enforced by xAI as of 2026-10-02."
128
- })
924
+ }),
925
+ searchBudget: XaiSearchBudgetSchema.optional()
926
+ }).superRefine((options, ctx) => {
927
+ const budget = options.searchBudget;
928
+ if (budget === void 0) return;
929
+ const kinds = new Set((options.tools ?? []).map((tool) => tool.type));
930
+ const need = (ceiling, tool) => {
931
+ if (budget[ceiling] !== void 0 && !kinds.has(tool)) {
932
+ ctx.addIssue({
933
+ code: "custom",
934
+ path: ["searchBudget", ceiling],
935
+ message: `searchBudget.${ceiling} requires ${tool === "x_search" ? "an" : "a"} ${tool} tool in tools.`
936
+ });
937
+ }
938
+ };
939
+ need("maxWebSearchCalls", "web_search");
940
+ need("maxXItems", "x_search");
129
941
  }).meta({
130
942
  title: "xAI Provider Options",
131
943
  description: "Allowlisted xAI provider options."
@@ -133,17 +945,17 @@ var XaiProviderOptionsSchema = zod.z.strictObject({
133
945
 
134
946
  // src/model-config/grok-4-5.ts
135
947
  var Grok45ConfigSchema = zod.z.strictObject({
136
- temperature: zod.z.number().optional().meta({
948
+ temperature: zod.z.number().min(0).max(2).optional().meta({
137
949
  title: "Temperature",
138
- description: "Sampling temperature forwarded verbatim to grok-4.5."
950
+ description: 'Sampling temperature forwarded verbatim to grok-4.5, from 0 to 2 (the range xAI documents for the Responses API: "between 0 and 2"); a value outside it is rejected.'
139
951
  }),
140
- topP: zod.z.number().optional().meta({
952
+ topP: zod.z.number().min(0).max(1).optional().meta({
141
953
  title: "Top P",
142
- description: "Nucleus sampling parameter forwarded verbatim to grok-4.5."
954
+ description: "Nucleus sampling probability forwarded verbatim to grok-4.5, from 0 to 1 (xAI documents no range for it; a probability mass outside 0 to 1 is meaningless); a value outside it is rejected."
143
955
  }),
144
- maxOutputTokens: zod.z.number().int().positive().optional().meta({
956
+ maxOutputTokens: core.maxOutputTokensSchema(XAI_MODEL_LIMITS["grok-4.5"]).optional().meta({
145
957
  title: "Max Output Tokens",
146
- description: "Maximum output token cap for grok-4.5. No artificial ceiling \u2014 xAI accepts arbitrarily large values (live-verified); truncation surfaces as finishReason:'length', not an error."
958
+ description: "Maximum output token cap for grok-4.5, including reasoning tokens. No ceiling is applied: xAI documents no output limit and has accepted very large values (live-verified). Truncation surfaces as finishReason:'length', not an error."
147
959
  }),
148
960
  reasoning: zod.z.strictObject({
149
961
  effort: zod.z.enum(["low", "medium", "high"]).meta({
@@ -158,9 +970,9 @@ var Grok45ConfigSchema = zod.z.strictObject({
158
970
  title: "Service Tier",
159
971
  description: "xAI priority processing for grok-4.5, billed at 2\xD7 on input, cached input, and output tokens. Live-verified 2026-09-25."
160
972
  }),
161
- timeoutMs: zod.z.number().int().positive().optional().meta({
973
+ timeoutMs: zod.z.number().int().positive().max(XAI_MAX_TIMEOUT_MS).optional().meta({
162
974
  title: "Timeout",
163
- description: "Logical request timeout in milliseconds."
975
+ description: "Logical request timeout in milliseconds (at most 2147478647)."
164
976
  }),
165
977
  providerOptions: zod.z.strictObject({
166
978
  xai: XaiProviderOptionsSchema.optional()
@@ -174,17 +986,17 @@ var Grok45ConfigSchema = zod.z.strictObject({
174
986
  examples: [{ reasoning: { effort: "high" } }]
175
987
  });
176
988
  var Grok46ConfigSchema = zod.z.strictObject({
177
- temperature: zod.z.number().optional().meta({
989
+ temperature: zod.z.number().min(0).max(2).optional().meta({
178
990
  title: "Temperature",
179
- description: "Sampling temperature forwarded verbatim to grok-4.6."
991
+ description: 'Sampling temperature forwarded verbatim to grok-4.6, from 0 to 2 (the range xAI documents for the Responses API: "between 0 and 2"); a value outside it is rejected.'
180
992
  }),
181
- topP: zod.z.number().optional().meta({
993
+ topP: zod.z.number().min(0).max(1).optional().meta({
182
994
  title: "Top P",
183
- description: "Nucleus sampling parameter forwarded verbatim to grok-4.6."
995
+ description: "Nucleus sampling probability forwarded verbatim to grok-4.6, from 0 to 1 (xAI documents no range for it; a probability mass outside 0 to 1 is meaningless); a value outside it is rejected."
184
996
  }),
185
- maxOutputTokens: zod.z.number().int().positive().optional().meta({
997
+ maxOutputTokens: core.maxOutputTokensSchema(XAI_MODEL_LIMITS["grok-4.6"]).optional().meta({
186
998
  title: "Max Output Tokens",
187
- description: "Maximum output token cap for grok-4.6. No artificial ceiling \u2014 xAI accepts arbitrarily large values; truncation surfaces as finishReason:'length', not an error."
999
+ description: "Maximum output token cap for grok-4.6, including reasoning tokens. No ceiling is applied: xAI documents no output limit and has accepted very large values (live-verified). Truncation surfaces as finishReason:'length', not an error."
188
1000
  }),
189
1001
  reasoning: zod.z.strictObject({
190
1002
  effort: zod.z.enum(["low", "medium", "high", "xhigh"]).meta({
@@ -199,9 +1011,9 @@ var Grok46ConfigSchema = zod.z.strictObject({
199
1011
  title: "Service Tier",
200
1012
  description: 'xAI priority processing for grok-4.6 (Responses `service_tier: "priority"`). Echo live-verified 2026-08-12. Bills at 2\xD7 after the cache discount (uncached standard-list 2\xD7 confirmed by live ticks; cached/long-context legs follow the official 2\xD7 rule). Omitted requests stay on xAI default. "flex"/"standard"/"batch" are rejected.'
201
1013
  }),
202
- timeoutMs: zod.z.number().int().positive().optional().meta({
1014
+ timeoutMs: zod.z.number().int().positive().max(XAI_MAX_TIMEOUT_MS).optional().meta({
203
1015
  title: "Timeout",
204
- description: "Logical request timeout in milliseconds."
1016
+ description: "Logical request timeout in milliseconds (at most 2147478647)."
205
1017
  }),
206
1018
  providerOptions: zod.z.strictObject({
207
1019
  xai: XaiProviderOptionsSchema.optional()
@@ -215,17 +1027,17 @@ var Grok46ConfigSchema = zod.z.strictObject({
215
1027
  examples: [{ reasoning: { effort: "high" } }]
216
1028
  });
217
1029
  var Grok47ConfigSchema = zod.z.strictObject({
218
- temperature: zod.z.number().optional().meta({
1030
+ temperature: zod.z.number().min(0).max(2).optional().meta({
219
1031
  title: "Temperature",
220
- description: "Sampling temperature forwarded verbatim to grok-4.7."
1032
+ description: 'Sampling temperature forwarded verbatim to grok-4.7, from 0 to 2 (the range xAI documents for the Responses API: "between 0 and 2"); a value outside it is rejected.'
221
1033
  }),
222
- topP: zod.z.number().optional().meta({
1034
+ topP: zod.z.number().min(0).max(1).optional().meta({
223
1035
  title: "Top P",
224
- description: "Nucleus sampling parameter forwarded verbatim to grok-4.7."
1036
+ description: "Nucleus sampling probability forwarded verbatim to grok-4.7, from 0 to 1 (xAI documents no range for it; a probability mass outside 0 to 1 is meaningless); a value outside it is rejected."
225
1037
  }),
226
- maxOutputTokens: zod.z.number().int().positive().optional().meta({
1038
+ maxOutputTokens: core.maxOutputTokensSchema(XAI_MODEL_LIMITS["grok-4.7"]).optional().meta({
227
1039
  title: "Max Output Tokens",
228
- description: "Maximum output token cap for grok-4.7. No artificial ceiling \u2014 xAI accepts arbitrarily large values; truncation surfaces as finishReason:'length', not an error."
1040
+ description: "Maximum output token cap for grok-4.7, including reasoning tokens. No ceiling is applied: xAI documents no output limit and has accepted very large values (live-verified). Truncation surfaces as finishReason:'length', not an error."
229
1041
  }),
230
1042
  reasoning: zod.z.strictObject({
231
1043
  effort: zod.z.enum(["low", "medium", "high", "xhigh"]).meta({
@@ -240,9 +1052,9 @@ var Grok47ConfigSchema = zod.z.strictObject({
240
1052
  title: "Service Tier",
241
1053
  description: 'xAI priority processing for grok-4.7 (Responses `service_tier: "priority"`). Bills at 2\xD7 after the cache discount (uncached standard-list 2\xD7 cached/long-context legs follow the official 2\xD7 rule). Omitted requests stay on xAI default. "flex"/"standard"/"batch" are rejected.'
242
1054
  }),
243
- timeoutMs: zod.z.number().int().positive().optional().meta({
1055
+ timeoutMs: zod.z.number().int().positive().max(XAI_MAX_TIMEOUT_MS).optional().meta({
244
1056
  title: "Timeout",
245
- description: "Logical request timeout in milliseconds."
1057
+ description: "Logical request timeout in milliseconds (at most 2147478647)."
246
1058
  }),
247
1059
  providerOptions: zod.z.strictObject({
248
1060
  xai: XaiProviderOptionsSchema.optional()
@@ -260,15 +1072,15 @@ var Grok47ConfigSchema = zod.z.strictObject({
260
1072
  var grok45ModelDescriptor = {
261
1073
  model: "grok-4.5",
262
1074
  provider: "xai",
1075
+ limits: XAI_MODEL_LIMITS["grok-4.5"],
263
1076
  pricingFamily: "grok-4.5",
264
1077
  capabilities: {
1078
+ inputMimeTypes: XAI_INPUT_MIME_TYPES,
265
1079
  reasoning: true,
266
1080
  reasoningApi: "level",
267
1081
  admittedReasoningEfforts: ["low", "medium", "high"],
268
1082
  structuredOutput: true,
269
1083
  nativeStructuredOutput: true,
270
- vision: true,
271
- audioInput: false,
272
1084
  sampling: "tunable",
273
1085
  caching: { explicit: false, minTokens: 0 },
274
1086
  grounding: true,
@@ -277,21 +1089,22 @@ var grok45ModelDescriptor = {
277
1089
  serviceTiers: ["priority"]
278
1090
  },
279
1091
  configSchema: Grok45ConfigSchema,
1092
+ configKeys: core.toConfigKeys(Grok45ConfigSchema),
280
1093
  configJsonSchema: core.toConfigJsonSchema(Grok45ConfigSchema),
281
1094
  validateConfig: core.zodToStandardSchema(Grok45ConfigSchema)
282
1095
  };
283
1096
  var grok46ModelDescriptor = {
284
1097
  model: "grok-4.6",
285
1098
  provider: "xai",
1099
+ limits: XAI_MODEL_LIMITS["grok-4.6"],
286
1100
  pricingFamily: "grok-4.6",
287
1101
  capabilities: {
1102
+ inputMimeTypes: XAI_INPUT_MIME_TYPES,
288
1103
  reasoning: true,
289
1104
  reasoningApi: "level",
290
1105
  admittedReasoningEfforts: ["low", "medium", "high", "xhigh"],
291
1106
  structuredOutput: true,
292
1107
  nativeStructuredOutput: true,
293
- vision: true,
294
- audioInput: false,
295
1108
  sampling: "tunable",
296
1109
  caching: { explicit: false, minTokens: 0 },
297
1110
  grounding: true,
@@ -300,30 +1113,33 @@ var grok46ModelDescriptor = {
300
1113
  serviceTiers: ["priority"]
301
1114
  },
302
1115
  configSchema: Grok46ConfigSchema,
1116
+ configKeys: core.toConfigKeys(Grok46ConfigSchema),
303
1117
  configJsonSchema: core.toConfigJsonSchema(Grok46ConfigSchema),
304
1118
  validateConfig: core.zodToStandardSchema(Grok46ConfigSchema)
305
1119
  };
306
1120
  var grok47ModelDescriptor = {
307
1121
  model: "grok-4.7",
308
1122
  provider: "xai",
1123
+ limits: XAI_MODEL_LIMITS["grok-4.7"],
309
1124
  pricingFamily: "grok-4.7",
310
1125
  capabilities: {
1126
+ inputMimeTypes: XAI_INPUT_MIME_TYPES,
311
1127
  reasoning: true,
312
1128
  reasoningApi: "level",
313
1129
  admittedReasoningEfforts: ["low", "medium", "high", "xhigh"],
314
1130
  structuredOutput: true,
315
1131
  nativeStructuredOutput: true,
316
- vision: true,
317
- audioInput: false,
318
1132
  sampling: "tunable",
319
1133
  caching: { explicit: false, minTokens: 0 },
320
1134
  grounding: true,
321
1135
  structuredOutputWithTools: true,
322
1136
  functionCalling: true,
323
- statelessReasoningReplay: true,
1137
+ continuation: "state",
1138
+ providerState: true,
324
1139
  serviceTiers: ["priority"]
325
1140
  },
326
1141
  configSchema: Grok47ConfigSchema,
1142
+ configKeys: core.toConfigKeys(Grok47ConfigSchema),
327
1143
  configJsonSchema: core.toConfigJsonSchema(Grok47ConfigSchema),
328
1144
  validateConfig: core.zodToStandardSchema(Grok47ConfigSchema)
329
1145
  };
@@ -333,89 +1149,48 @@ var xaiModelDescriptors = [
333
1149
  grok47ModelDescriptor
334
1150
  ];
335
1151
  var xaiRegistry = core.createModelRegistry(xaiModelDescriptors);
336
- var JSON_SCHEMA_TYPES = /* @__PURE__ */ new Set([
337
- "string",
338
- "number",
339
- "integer",
340
- "boolean",
341
- "object",
342
- "array",
343
- "null"
344
- ]);
345
- var SINGLE_SCHEMA_KEYWORDS = [
346
- "additionalProperties",
347
- "additionalItems",
348
- "items",
349
- "contains",
350
- "not",
351
- "if",
352
- "then",
353
- "else",
354
- "propertyNames",
355
- "unevaluatedProperties",
356
- "unevaluatedItems",
357
- "contentSchema"
358
- ];
359
- var SCHEMA_ARRAY_KEYWORDS = ["items", "prefixItems", "anyOf", "oneOf", "allOf"];
360
- var SCHEMA_MAP_KEYWORDS = [
361
- "properties",
362
- "patternProperties",
363
- "dependentSchemas",
364
- // Draft-07 `dependencies`: schema-valued entries are walked; array-valued
365
- // entries are property-name lists and are skipped by the object check.
366
- "dependencies",
367
- "$defs",
368
- "definitions"
369
- ];
370
- function isPlainObject(value) {
371
- return value !== null && typeof value === "object" && !Array.isArray(value);
372
- }
373
- function badSchema(message) {
374
- return new core.LlmError(message, { kind: "bad_request", retryable: false, provider: "xai" });
375
- }
376
- function assertNode(node, path) {
377
- const at = path.length > 0 ? path : "<root>";
378
- if ("nullable" in node) {
379
- throw badSchema(
380
- `xAI structured output takes standard JSON Schema, which has no \`nullable\` keyword (found at \`${at}\`). xAI ignores it, so the field would be required and non-null. List 'null' in \`type\` instead, e.g. type: ['string', 'null'].`
381
- );
382
- }
383
- const type = node["type"];
384
- if (type !== void 0) {
385
- const names = Array.isArray(type) ? type : [type];
386
- for (const name of names) {
387
- if (typeof name !== "string" || !JSON_SCHEMA_TYPES.has(name)) {
388
- throw badSchema(
389
- `xAI structured output takes standard JSON Schema; \`type\` ${JSON.stringify(
390
- name
391
- )} at \`${at}\` is not a JSON Schema type. Use lowercase string, number, integer, boolean, object, array or null.`
392
- );
393
- }
394
- }
395
- }
396
- const child = (segment) => path.length > 0 ? `${path}.${segment}` : segment;
397
- for (const keyword of SCHEMA_MAP_KEYWORDS) {
398
- const map = node[keyword];
399
- if (!isPlainObject(map)) continue;
400
- for (const [key, member] of Object.entries(map)) {
401
- if (isPlainObject(member)) assertNode(member, child(`${keyword}.${key}`));
402
- }
403
- }
404
- for (const keyword of SCHEMA_ARRAY_KEYWORDS) {
405
- const members = node[keyword];
406
- if (!Array.isArray(members)) continue;
407
- members.forEach((member, index) => {
408
- if (isPlainObject(member)) assertNode(member, child(`${keyword}[${index}]`));
409
- });
410
- }
411
- for (const keyword of SINGLE_SCHEMA_KEYWORDS) {
412
- const member = node[keyword];
413
- if (isPlainObject(member)) assertNode(member, child(keyword));
414
- }
415
- }
416
- function assertXaiOutputJsonSchema(schema) {
417
- if (isPlainObject(schema)) assertNode(schema, "");
418
- }
1152
+
1153
+ // src/json-schema.ts
1154
+ var XAI_JSON_SCHEMA_PROFILE = {
1155
+ provider: "xai",
1156
+ keywords: [
1157
+ "type",
1158
+ "properties",
1159
+ "required",
1160
+ "additionalProperties",
1161
+ "enum",
1162
+ "const",
1163
+ "anyOf",
1164
+ "$ref",
1165
+ "$defs",
1166
+ "items",
1167
+ "prefixItems",
1168
+ "minItems",
1169
+ "maxItems",
1170
+ "minimum",
1171
+ "maximum",
1172
+ "exclusiveMinimum",
1173
+ "exclusiveMaximum",
1174
+ "minLength",
1175
+ "maxLength",
1176
+ "minProperties",
1177
+ "maxProperties",
1178
+ "pattern",
1179
+ "format"
1180
+ ],
1181
+ formats: ["date", "time", "date-time", "email", "uuid", "ipv4", "ipv6", "uri"],
1182
+ limits: {
1183
+ minLength: 2048,
1184
+ maxLength: 2048,
1185
+ minItems: 256,
1186
+ maxItems: 256,
1187
+ minProperties: 64,
1188
+ maxProperties: 64
1189
+ },
1190
+ circularRefs: false,
1191
+ booleanItems: false,
1192
+ patternSubset: true
1193
+ };
419
1194
  var xaiPricingVersion = "xai-2026-09-25";
420
1195
  var XAI_TOOL_RATE_MICRO_USD = {
421
1196
  web_search_calls: 5e3,
@@ -429,6 +1204,51 @@ var XAI_TOOL_COUNTER_KEYS = [
429
1204
  ];
430
1205
  var X_SEARCH_ITEM_COUNTERS = ["x_posts_fetched", "x_users_fetched"];
431
1206
  var LONG_CONTEXT_THRESHOLD = 2e5;
1207
+ var XAI_SERVER_TOOL_COUNTERS = Object.freeze({
1208
+ web_search_calls: "priced",
1209
+ x_posts_fetched: "priced",
1210
+ x_users_fetched: "priced",
1211
+ x_search_calls: "superseded",
1212
+ code_interpreter_calls: "fee_unpriced",
1213
+ file_search_calls: "fee_unpriced",
1214
+ document_search_calls: "fee_unpriced",
1215
+ image_generation_calls: "fee_unpriced",
1216
+ mcp_calls: "token_only"
1217
+ });
1218
+ function classifyUnpricedXaiToolCounters(usage) {
1219
+ const candidates = /* @__PURE__ */ new Map();
1220
+ const raw = usage.raw;
1221
+ if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
1222
+ const nested = raw["server_side_tool_usage_details"];
1223
+ if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) {
1224
+ for (const [key, value] of Object.entries(nested)) candidates.set(key, value);
1225
+ }
1226
+ }
1227
+ for (const [key, value] of Object.entries(usage.details)) {
1228
+ if (Object.hasOwn(XAI_SERVER_TOOL_COUNTERS, key) && !candidates.has(key)) {
1229
+ candidates.set(key, value);
1230
+ }
1231
+ }
1232
+ const feeUnpriced = [];
1233
+ const unknown = [];
1234
+ for (const [key, value] of candidates) {
1235
+ if (typeof value !== "number" || !(value > 0)) continue;
1236
+ if (!Object.hasOwn(XAI_SERVER_TOOL_COUNTERS, key)) unknown.push(key);
1237
+ else if (XAI_SERVER_TOOL_COUNTERS[key] === "fee_unpriced") feeUnpriced.push(key);
1238
+ }
1239
+ return { feeUnpriced, unknown };
1240
+ }
1241
+ function unpricedXaiToolCounters(usage) {
1242
+ const { feeUnpriced, unknown } = classifyUnpricedXaiToolCounters(usage);
1243
+ return [...feeUnpriced, ...unknown];
1244
+ }
1245
+ var XAI_ESTIMATED_USAGE_KEY = "usage_estimated";
1246
+ var TICKS_PER_MICRO_USD = 1e4;
1247
+ function providerReportedCost(usage) {
1248
+ const ticks = usage.details["cost_in_usd_ticks"];
1249
+ if (typeof ticks !== "number" || !Number.isFinite(ticks) || ticks < 0) return void 0;
1250
+ return { microUsd: Math.round(ticks / TICKS_PER_MICRO_USD) };
1251
+ }
432
1252
  var XAI_PRICING = Object.freeze({
433
1253
  // ── grok-4.5 ── $2.00/$6.00 (<200k), $4.00/$12.00 (>=200k); cached $0.30/$0.60
434
1254
  "grok-4.5": {
@@ -506,6 +1326,11 @@ function scaleRates(rates, factor) {
506
1326
  return scaled;
507
1327
  }
508
1328
  function computeXaiCost(model, usage, tier) {
1329
+ const cost = priceXaiCall(model, usage, tier);
1330
+ const providerReported = providerReportedCost(usage);
1331
+ return providerReported === void 0 ? cost : { ...cost, providerReported };
1332
+ }
1333
+ function priceXaiCall(model, usage, tier) {
509
1334
  const listed = lookupConcreteRates(model, tier);
510
1335
  if (listed === void 0) {
511
1336
  return core.computeCost(model, usage, tier, lookupConcreteRates, xaiPricingVersion);
@@ -541,11 +1366,12 @@ function computeXaiCost(model, usage, tier) {
541
1366
  return sum + Math.round(count * XAI_TOOL_RATE_MICRO_USD[key]);
542
1367
  }, 0);
543
1368
  const microUsd = inputCost + cachedCost + outputCost + toolsCost;
1369
+ const unpricedCounters = unpricedXaiToolCounters(usage);
544
1370
  return {
545
1371
  microUsd,
546
1372
  usd: microUsd / 1e6,
547
1373
  pricingVersion: xaiPricingVersion,
548
- confidence: missingWebCounter || attachmentUnpinned ? "estimated" : "exact",
1374
+ confidence: missingWebCounter || attachmentUnpinned || unpricedCounters.length > 0 || usage.details[XAI_ESTIMATED_USAGE_KEY] === 1 ? "estimated" : "exact",
549
1375
  details: {
550
1376
  input: inputCost,
551
1377
  cached: cachedCost,
@@ -579,22 +1405,42 @@ function isXaiMessageItem(item) {
579
1405
  function isXaiReasoningItem(item) {
580
1406
  return item.type === "reasoning" && Array.isArray(item.summary);
581
1407
  }
1408
+ function partText(part) {
1409
+ return isPlainRecord(part) && typeof part["text"] === "string" ? part["text"] : void 0;
1410
+ }
1411
+ function boundedNote(value) {
1412
+ return value.length > 500 ? `${value.slice(0, 500)}...` : value;
1413
+ }
582
1414
  function badXaiRequest(message) {
583
1415
  return new core.LlmError(message, { kind: "bad_request", retryable: false });
584
1416
  }
585
- var ALLOWED_XAI_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/jpg", "image/png"]);
586
1417
  var MAX_XAI_INLINE_IMAGE_BYTES = 20 * 1024 * 1024;
1418
+ function decodedBase64Length(data) {
1419
+ const plain = (length2, tail) => {
1420
+ let padding = 0;
1421
+ while (padding < 2 && tail.charCodeAt(tail.length - 1 - padding) === 61)
1422
+ padding += 1;
1423
+ return Math.floor((length2 - padding) * 3 / 4);
1424
+ };
1425
+ const upper = plain(data.length, data);
1426
+ if (upper <= MAX_XAI_INLINE_IMAGE_BYTES) return upper;
1427
+ let length = 0;
1428
+ let end = data.length;
1429
+ for (let i = 0; i < data.length; i += 1) {
1430
+ const c = data.charCodeAt(i);
1431
+ if (c !== 32 && c !== 10 && c !== 13 && c !== 9) {
1432
+ length += 1;
1433
+ end = i + 1;
1434
+ }
1435
+ }
1436
+ return plain(length, data.slice(0, end));
1437
+ }
587
1438
  function mapPart(p) {
588
1439
  switch (p.kind) {
589
1440
  case "text":
590
1441
  return { type: "input_text", text: p.text };
591
1442
  case "inline-media": {
592
- if (!ALLOWED_XAI_IMAGE_MIME_TYPES.has(p.mimeType)) {
593
- throw badXaiRequest(
594
- `xAI vision only supports image/jpeg and image/png; got mimeType "${p.mimeType}".`
595
- );
596
- }
597
- const byteLength = Buffer.from(p.data, "base64").length;
1443
+ const byteLength = decodedBase64Length(p.data);
598
1444
  if (byteLength > MAX_XAI_INLINE_IMAGE_BYTES) {
599
1445
  throw badXaiRequest(
600
1446
  `xAI inline images must be at most 20 MiB; got ${byteLength} bytes.`
@@ -604,8 +1450,7 @@ function mapPart(p) {
604
1450
  }
605
1451
  case "file-uri": {
606
1452
  const isPublicHttpUrl = p.uri.startsWith("http://") || p.uri.startsWith("https://");
607
- const isAllowedImageType = ALLOWED_XAI_IMAGE_MIME_TYPES.has(p.mimeType);
608
- if (!isPublicHttpUrl || !isAllowedImageType) {
1453
+ if (!isPublicHttpUrl) {
609
1454
  throw badXaiRequest(
610
1455
  `xAI only accepts public http(s) image URLs via FileUriPart; got scheme of "${p.uri}" / mimeType "${p.mimeType}".`
611
1456
  );
@@ -637,9 +1482,75 @@ var XAI_PROVIDER_OPTION_KEYS = /* @__PURE__ */ new Set([
637
1482
  "tools",
638
1483
  "parallelToolCalls",
639
1484
  "toolChoice",
640
- "maxTurns"
1485
+ "maxTurns",
1486
+ "searchBudget"
641
1487
  ]);
1488
+ var XAI_DEFAULT_SCHEMA_NAME = "structured_output";
1489
+ var XAI_SCHEMA_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
642
1490
  var XAI_SERVER_TOOL_CHOICES = /* @__PURE__ */ new Set(["auto", "required", "none"]);
1491
+ var XAI_SEARCH_BUDGET_KEYS = ["maxWebSearchCalls", "maxXItems"];
1492
+ function mapXaiSearchBudget(value, tools, model) {
1493
+ if (!isPlainRecord(value)) {
1494
+ throw badXaiRequest(
1495
+ `providerOptions.xai.searchBudget must be an object for model "${model}".`
1496
+ );
1497
+ }
1498
+ const unknown = Object.keys(value).filter(
1499
+ (key) => !XAI_SEARCH_BUDGET_KEYS.includes(key)
1500
+ );
1501
+ if (unknown.length > 0) {
1502
+ throw badXaiRequest(
1503
+ `providerOptions.xai.searchBudget contains unsupported keys [${unknown.join(
1504
+ ", "
1505
+ )}] for model "${model}". Allowed keys: ${XAI_SEARCH_BUDGET_KEYS.join(", ")}.`
1506
+ );
1507
+ }
1508
+ const budget = {};
1509
+ for (const key of XAI_SEARCH_BUDGET_KEYS) {
1510
+ const entry = value[key];
1511
+ if (entry === void 0) continue;
1512
+ if (typeof entry !== "number" || !Number.isInteger(entry) || entry < 1) {
1513
+ throw badXaiRequest(
1514
+ `providerOptions.xai.searchBudget.${key} must be an integer >= 1 for model "${model}".`
1515
+ );
1516
+ }
1517
+ budget[key] = entry;
1518
+ }
1519
+ if (budget.maxWebSearchCalls === void 0 && budget.maxXItems === void 0) {
1520
+ throw badXaiRequest(
1521
+ `providerOptions.xai.searchBudget must set maxWebSearchCalls or maxXItems for model "${model}".`
1522
+ );
1523
+ }
1524
+ const hasTool = (type) => tools?.some((tool) => tool["type"] === type) === true;
1525
+ if (budget.maxWebSearchCalls !== void 0 && !hasTool("web_search")) {
1526
+ throw badXaiRequest(
1527
+ `providerOptions.xai.searchBudget.maxWebSearchCalls requires a web_search tool in providerOptions.xai.tools for model "${model}".`
1528
+ );
1529
+ }
1530
+ if (budget.maxXItems !== void 0 && !hasTool("x_search")) {
1531
+ throw badXaiRequest(
1532
+ `providerOptions.xai.searchBudget.maxXItems requires an x_search tool in providerOptions.xai.tools for model "${model}".`
1533
+ );
1534
+ }
1535
+ return budget;
1536
+ }
1537
+ function exceededSearchBudget(budget, details) {
1538
+ const over = [];
1539
+ const webCalls = details[WEB_SEARCH_COUNTER];
1540
+ if (budget.maxWebSearchCalls !== void 0 && webCalls !== void 0 && webCalls > budget.maxWebSearchCalls) {
1541
+ over.push(
1542
+ `${WEB_SEARCH_COUNTER} ${webCalls} > maxWebSearchCalls ${budget.maxWebSearchCalls}`
1543
+ );
1544
+ }
1545
+ if (budget.maxXItems !== void 0) {
1546
+ const reported = X_SEARCH_ITEM_COUNTERS.filter((key) => details[key] !== void 0);
1547
+ const items = reported.reduce((sum, key) => sum + details[key], 0);
1548
+ if (reported.length > 0 && items > budget.maxXItems) {
1549
+ over.push(`X items ${items} > maxXItems ${budget.maxXItems}`);
1550
+ }
1551
+ }
1552
+ return over;
1553
+ }
643
1554
  function mapXaiProviderOptions(xaiOpts, model) {
644
1555
  if (xaiOpts === void 0) {
645
1556
  return {};
@@ -654,7 +1565,7 @@ function mapXaiProviderOptions(xaiOpts, model) {
654
1565
  throw badXaiRequest(
655
1566
  `providerOptions.xai contains unsupported keys [${unknownKeys.join(
656
1567
  ", "
657
- )}] for model "${model}". Allowed keys: promptCacheKey, tools, parallelToolCalls, toolChoice, maxTurns.`
1568
+ )}] for model "${model}". Allowed keys: promptCacheKey, tools, parallelToolCalls, toolChoice, maxTurns, searchBudget.`
658
1569
  );
659
1570
  }
660
1571
  const mapped = {};
@@ -705,15 +1616,25 @@ function mapXaiProviderOptions(xaiOpts, model) {
705
1616
  }
706
1617
  mapped.maxTurns = maxTurns;
707
1618
  }
1619
+ if (xaiOpts["searchBudget"] !== void 0) {
1620
+ mapped.searchBudget = mapXaiSearchBudget(xaiOpts["searchBudget"], mapped.tools, model);
1621
+ }
708
1622
  return mapped;
709
1623
  }
710
1624
  function parseXaiReplayState(value, model) {
711
1625
  if (value === void 0) return void 0;
712
- if (!isPlainRecord(value) || value["model"] !== model || !Array.isArray(value["input"]) || value["input"].length === 0 || value["input"].some(
1626
+ const keys = isPlainRecord(value) ? Object.keys(value) : [];
1627
+ if (keys.some((key) => key !== "xai")) {
1628
+ throw badXaiRequest(
1629
+ `transientProviderState has unexpected key(s) [${keys.join(", ")}]; xAI state is exactly { xai: { model, input } } (another provider's state is rejected).`
1630
+ );
1631
+ }
1632
+ const inner = isPlainRecord(value) ? value["xai"] : void 0;
1633
+ if (!isPlainRecord(inner) || inner["model"] !== model || !Array.isArray(inner["input"]) || inner["input"].length === 0 || inner["input"].some(
713
1634
  (item) => !isPlainRecord(item) || typeof item["type"] !== "string" && typeof item["role"] !== "string"
714
1635
  )) {
715
1636
  throw badXaiRequest(
716
- `transientProviderState must contain the full xAI wire input for model "${model}".`
1637
+ `transientProviderState must be { xai: { model, input } } with the full xAI wire input, bound to the requested model "${model}".`
717
1638
  );
718
1639
  }
719
1640
  return value;
@@ -847,35 +1768,56 @@ function isXaiSafetyCheckBody(rawErr) {
847
1768
  const text = extractXaiErrorBodyText(rawErr);
848
1769
  return text !== void 0 && text.startsWith(XAI_SAFETY_CHECK_MESSAGE_PREFIX);
849
1770
  }
850
- var XAI_TRANSPORT_ERROR_PATTERN = /connection error|econnreset|econnrefused|etimedout|eai_again|epipe|socket hang up|fetch failed/i;
851
- function matchesXaiTransportSignature(err) {
852
- if (!(err instanceof Error)) return false;
853
- if (XAI_TRANSPORT_ERROR_PATTERN.test(err.message)) return true;
854
- const code = err.code;
855
- return typeof code === "string" && XAI_TRANSPORT_ERROR_PATTERN.test(code);
856
- }
857
- function isXaiTransportError(rawErr) {
858
- if (!(rawErr instanceof Error)) return false;
859
- const ctorName = rawErr.constructor.name;
860
- if (ctorName === "APIConnectionError" || ctorName === "APIConnectionTimeoutError") {
861
- return true;
862
- }
863
- if (matchesXaiTransportSignature(rawErr)) return true;
864
- const cause = rawErr.cause;
865
- if (cause instanceof Error) {
866
- const causeCtorName = cause.constructor.name;
867
- if (causeCtorName === "APIConnectionError" || causeCtorName === "APIConnectionTimeoutError") {
868
- return true;
869
- }
870
- if (matchesXaiTransportSignature(cause)) return true;
871
- }
872
- return false;
1771
+ var XAI_CREDITS_EXHAUSTED_BODY = /^Your team \S+ has either used all available credits or reached its monthly spending limit/;
1772
+ function isXaiCreditsExhaustedBody(rawErr) {
1773
+ const text = extractXaiErrorBodyText(rawErr);
1774
+ return text !== void 0 && XAI_CREDITS_EXHAUSTED_BODY.test(text);
1775
+ }
1776
+ function isOpenAiSdkConnectionError(rawErr) {
1777
+ return rawErr instanceof Error && (rawErr.constructor.name === "APIConnectionError" || rawErr.constructor.name === "APIConnectionTimeoutError");
873
1778
  }
874
- function classifyXaiError(rawErr) {
1779
+ var UNDICI_HEADERS_TIMEOUT_CODE = "UND_ERR_HEADERS_TIMEOUT";
1780
+ var UNDICI_BODY_TIMEOUT_CODE = "UND_ERR_BODY_TIMEOUT";
1781
+ var SDK_DEADLINE_SLACK_MS = 5;
1782
+ function isSdkDeadline(rawErr, deadline) {
1783
+ if (deadline === void 0) return false;
1784
+ if (!(rawErr instanceof Error) || rawErr.constructor.name !== "APIConnectionTimeoutError") {
1785
+ return false;
1786
+ }
1787
+ const causes = core.causeChain(rawErr).slice(1);
1788
+ if (!causes.every((e) => e.name === "AbortError")) return false;
1789
+ return deadline.elapsedMs >= deadline.timeoutMs - SDK_DEADLINE_SLACK_MS;
1790
+ }
1791
+ function xaiTransportTimeoutKind(rawErr, deadline) {
1792
+ const chain = core.causeChain(rawErr);
1793
+ const has = (code, name) => chain.some((e) => {
1794
+ const o = e;
1795
+ return o.code === code || o.name === name;
1796
+ });
1797
+ if (has(UNDICI_HEADERS_TIMEOUT_CODE, "HeadersTimeoutError")) return "headers";
1798
+ if (has(UNDICI_BODY_TIMEOUT_CODE, "BodyTimeoutError")) return "body";
1799
+ if (isSdkDeadline(rawErr, deadline)) return "sdk";
1800
+ return void 0;
1801
+ }
1802
+ function classifyXaiError(rawErr, deadline, estimatedInputTokens = 0, requestTimeoutMs) {
875
1803
  if (rawErr instanceof core.LlmError) {
876
1804
  return rawErr;
877
1805
  }
1806
+ if (rawErr instanceof XaiStreamError) {
1807
+ return classifyStreamError(rawErr, estimatedInputTokens, requestTimeoutMs);
1808
+ }
878
1809
  const base = core.classifyError(rawErr);
1810
+ const transportTimeout = xaiTransportTimeoutKind(rawErr, deadline);
1811
+ if (transportTimeout !== void 0) {
1812
+ const which = transportTimeout === "sdk" ? "SDK deadline" : `transport ${transportTimeout} timer`;
1813
+ return new core.LlmError(`xAI request hit the ${which}: ${base.message}`, {
1814
+ kind: "timeout",
1815
+ retryable: false,
1816
+ reason: "transport_timeout",
1817
+ provider: "xai",
1818
+ cause: base.cause ?? rawErr
1819
+ });
1820
+ }
879
1821
  if (base.httpStatus === 400 && isXaiAuthFailureBody(rawErr)) {
880
1822
  return new core.LlmError(base.message, {
881
1823
  kind: "invalid_auth",
@@ -895,24 +1837,257 @@ function classifyXaiError(rawErr) {
895
1837
  cause: base.cause ?? rawErr
896
1838
  });
897
1839
  }
898
- if (base.kind === "unknown" && isXaiTransportError(rawErr)) {
899
- return new core.LlmError(base.message, {
900
- kind: "server",
901
- retryable: true,
1840
+ if ((base.httpStatus === 429 || base.httpStatus === 403) && isXaiCreditsExhaustedBody(rawErr)) {
1841
+ return new core.LlmError(
1842
+ (extractXaiErrorBodyText(rawErr) ?? base.message).replace(
1843
+ /^Your team \S+ /,
1844
+ "Your team "
1845
+ ),
1846
+ {
1847
+ kind: "rate_limited",
1848
+ retryable: false,
1849
+ reason: "credits_exhausted",
1850
+ httpStatus: base.httpStatus,
1851
+ provider: "xai",
1852
+ cause: base.cause ?? rawErr
1853
+ }
1854
+ );
1855
+ }
1856
+ if (base.kind === "unknown" && isOpenAiSdkConnectionError(rawErr)) {
1857
+ return new core.LlmError(base.message, {
1858
+ kind: "server",
1859
+ retryable: true,
1860
+ provider: "xai",
1861
+ cause: base.cause ?? rawErr
1862
+ });
1863
+ }
1864
+ return new core.LlmError(base.message, {
1865
+ kind: base.kind,
1866
+ retryable: base.retryable,
1867
+ ...base.httpStatus !== void 0 ? { httpStatus: base.httpStatus } : {},
1868
+ ...base.retryAfterMs !== void 0 ? { retryAfterMs: base.retryAfterMs } : {},
1869
+ provider: "xai",
1870
+ cause: base.cause ?? rawErr
1871
+ });
1872
+ }
1873
+ function classifyFailedResponseCode(code) {
1874
+ switch (code) {
1875
+ case "server_error":
1876
+ return { kind: "server", retryable: true };
1877
+ case "rate_limit_exceeded":
1878
+ return { kind: "rate_limited", retryable: true };
1879
+ case "bio_policy":
1880
+ case "misalignment_policy_violation":
1881
+ case "image_content_policy_violation":
1882
+ return { kind: "content_filter", retryable: false };
1883
+ case "invalid_prompt":
1884
+ case "data_residency_mismatch":
1885
+ case "invalid_image":
1886
+ case "invalid_image_format":
1887
+ case "invalid_base64_image":
1888
+ case "invalid_image_url":
1889
+ case "image_too_large":
1890
+ case "image_too_small":
1891
+ case "image_parse_error":
1892
+ case "invalid_image_mode":
1893
+ case "image_file_too_large":
1894
+ case "unsupported_image_media_type":
1895
+ case "empty_image_file":
1896
+ case "failed_to_download_image":
1897
+ case "image_file_not_found":
1898
+ return { kind: "bad_request", retryable: false };
1899
+ default:
1900
+ return { kind: "unknown", retryable: false };
1901
+ }
1902
+ }
1903
+ function estimatedStreamUsage(outputChars, inputTokens) {
1904
+ const outputTokens = Math.ceil(outputChars / 4);
1905
+ return {
1906
+ inputTokens,
1907
+ outputTokens,
1908
+ details: { input: inputTokens, output: outputTokens, [XAI_ESTIMATED_USAGE_KEY]: 1 },
1909
+ raw: {
1910
+ estimated: true,
1911
+ basis: "estimate: wire input characters / 4 for input (replayed state included, media not counted), received output characters / 4 for output; hidden reasoning is not counted",
1912
+ outputChars
1913
+ }
1914
+ };
1915
+ }
1916
+ function classifyStreamError(err, estimatedInputTokens, requestTimeoutMs) {
1917
+ const failure = err.failure;
1918
+ const { progressed, outputChars } = err.progress;
1919
+ const usage = err.terminalUsage !== void 0 ? mapUsage(err.terminalUsage) : progressed ? estimatedStreamUsage(outputChars, estimatedInputTokens) : void 0;
1920
+ const common = {
1921
+ provider: "xai",
1922
+ ...usage !== void 0 ? { usage } : {},
1923
+ ...err.servedServiceTier !== void 0 ? { servedServiceTier: err.servedServiceTier } : {},
1924
+ cause: err
1925
+ };
1926
+ switch (failure.kind) {
1927
+ case "ended_early":
1928
+ case "malformed":
1929
+ return new core.LlmError(err.message, {
1930
+ kind: "server",
1931
+ retryable: !progressed,
1932
+ ...common
1933
+ });
1934
+ case "cut": {
1935
+ const base = classifyXaiError(err.cause);
1936
+ return base.kind === "timeout" ? new core.LlmError(base.message, {
1937
+ kind: "timeout",
1938
+ retryable: false,
1939
+ ...base.reason !== void 0 ? { reason: base.reason } : {},
1940
+ ...common
1941
+ }) : new core.LlmError(err.message, { kind: "server", retryable: false, ...common });
1942
+ }
1943
+ case "deadline":
1944
+ return new core.LlmError(
1945
+ `xAI request hit the client deadline: the stream was still open after the ${requestTimeoutMs ?? failure.timeoutMs} ms request timeout`,
1946
+ {
1947
+ kind: "timeout",
1948
+ retryable: false,
1949
+ reason: "transport_timeout",
1950
+ ...common
1951
+ }
1952
+ );
1953
+ case "aborted": {
1954
+ const reason = err.cause;
1955
+ return reason instanceof core.LlmError ? new core.LlmError(reason.message, {
1956
+ kind: reason.kind,
1957
+ retryable: reason.retryable,
1958
+ ...reason.reason !== void 0 ? { reason: reason.reason } : {},
1959
+ ...common,
1960
+ cause: reason
1961
+ }) : new core.LlmError("Request aborted by caller", {
1962
+ kind: "aborted",
1963
+ retryable: false,
1964
+ ...common,
1965
+ cause: reason ?? err
1966
+ });
1967
+ }
1968
+ case "idle":
1969
+ return new core.LlmError(`xAI request hit the idle timeout: ${err.message}`, {
1970
+ kind: "timeout",
1971
+ retryable: false,
1972
+ reason: "transport_timeout",
1973
+ ...common
1974
+ });
1975
+ case "error_event": {
1976
+ const mapped = classifyFailedResponseCode(failure.code);
1977
+ return new core.LlmError(err.message, {
1978
+ kind: mapped.kind,
1979
+ retryable: mapped.kind === "server" && !progressed,
1980
+ mayHaveBilled: true,
1981
+ ...common
1982
+ });
1983
+ }
1984
+ case "not_event_stream":
1985
+ return new core.LlmError(err.message, {
1986
+ kind: "bad_request",
1987
+ retryable: false,
1988
+ mayHaveBilled: true,
1989
+ ...common
1990
+ });
1991
+ }
1992
+ }
1993
+ function failedResponseError(response, streamProgressed) {
1994
+ const reported = isPlainRecord(response.error) ? response.error : void 0;
1995
+ const code = typeof reported?.["code"] === "string" ? reported["code"] : void 0;
1996
+ const detail = typeof reported?.["message"] === "string" ? `: ${reported["message"]}` : "";
1997
+ const { kind, retryable } = response.status === "cancelled" ? { kind: "unknown", retryable: false } : classifyFailedResponseCode(code);
1998
+ return new core.LlmError(
1999
+ response.status === "cancelled" ? `xAI response reported status "cancelled"${detail}` : `xAI response failed${code !== void 0 ? ` (error.code "${code}")` : ""}${detail}`,
2000
+ {
2001
+ kind,
2002
+ retryable: retryable && !streamProgressed,
2003
+ mayHaveBilled: true,
902
2004
  provider: "xai",
903
- cause: base.cause ?? rawErr
904
- });
2005
+ // Usage is attached only when the failed response billed tokens.
2006
+ ...isPlainRecord(response.usage) && typeof response.usage.input_tokens === "number" && typeof response.usage.output_tokens === "number" ? { usage: mapUsage(response.usage) } : {},
2007
+ ...typeof response.service_tier === "string" && response.service_tier.length > 0 ? { servedServiceTier: response.service_tier } : {},
2008
+ cause: response.error ?? { status: response.status }
2009
+ }
2010
+ );
2011
+ }
2012
+ function estimateWireInputTokens(params) {
2013
+ const withoutImageBytes = (key, value) => key === "image_url" && typeof value === "string" && value.startsWith("data:") ? "" : value;
2014
+ try {
2015
+ const wire = JSON.stringify(
2016
+ {
2017
+ input: params.input,
2018
+ instructions: params.instructions,
2019
+ tools: params.tools,
2020
+ text: params.text
2021
+ },
2022
+ withoutImageBytes
2023
+ );
2024
+ return Math.ceil(wire.length / 4);
2025
+ } catch {
2026
+ return 0;
905
2027
  }
906
- return new core.LlmError(base.message, {
907
- kind: base.kind,
908
- retryable: base.retryable,
909
- ...base.httpStatus !== void 0 ? { httpStatus: base.httpStatus } : {},
910
- ...base.retryAfterMs !== void 0 ? { retryAfterMs: base.retryAfterMs } : {},
911
- provider: "xai",
912
- cause: base.cause ?? rawErr
2028
+ }
2029
+ var XAI_COUNT_TOKENS_TIMEOUT_MS = 6e4;
2030
+ function snapshotXaiTransport(transport) {
2031
+ if (transport === void 0) return void 0;
2032
+ const reject = (message) => new core.LlmError(`xaiAdapter: ${message}`, {
2033
+ kind: "bad_request",
2034
+ retryable: false,
2035
+ provider: "xai"
913
2036
  });
2037
+ const candidate = transport;
2038
+ if (candidate === null || typeof candidate !== "object") {
2039
+ throw reject("transport must be an object { fetch, fetchOptions?, idleTimeoutMs? }.");
2040
+ }
2041
+ if (typeof transport.fetch !== "function") {
2042
+ throw reject("transport.fetch must be a function.");
2043
+ }
2044
+ const idle = transport.idleTimeoutMs;
2045
+ if (idle !== void 0 && (typeof idle !== "number" || !Number.isInteger(idle) || idle < 1 || idle > XAI_MAX_TIMEOUT_MS)) {
2046
+ throw reject(
2047
+ `transport.idleTimeoutMs must be an integer from 1 to ${XAI_MAX_TIMEOUT_MS}.`
2048
+ );
2049
+ }
2050
+ const fetchOptions = transport.fetchOptions;
2051
+ if (fetchOptions !== void 0 && (fetchOptions === null || typeof fetchOptions !== "object" || Array.isArray(fetchOptions))) {
2052
+ throw reject("transport.fetchOptions must be an object.");
2053
+ }
2054
+ if (fetchOptions !== void 0) {
2055
+ for (const key of XAI_RESERVED_FETCH_OPTION_KEYS) {
2056
+ if (key in fetchOptions) {
2057
+ throw reject(
2058
+ `transport.fetchOptions.${key} is not supported; the request owns it.`
2059
+ );
2060
+ }
2061
+ }
2062
+ }
2063
+ return {
2064
+ fetch: transport.fetch,
2065
+ ...fetchOptions !== void 0 ? {
2066
+ fetchOptions: {
2067
+ ...fetchOptions
2068
+ }
2069
+ } : {},
2070
+ ...transport.idleTimeoutMs !== void 0 ? { idleTimeoutMs: transport.idleTimeoutMs } : {}
2071
+ };
914
2072
  }
915
2073
  function xaiAdapter(opts) {
2074
+ return xaiAdapterWithSeams(opts, {});
2075
+ }
2076
+ function xaiAdapterWithSeams(opts, seams) {
2077
+ if (opts?.client !== void 0 && opts.transport !== void 0) {
2078
+ throw new core.LlmError(
2079
+ "xaiAdapter: `transport` has no effect on an injected `client`; configure the transport on the client itself.",
2080
+ { kind: "bad_request", retryable: false, provider: "xai" }
2081
+ );
2082
+ }
2083
+ const transport = snapshotXaiTransport(opts?.transport);
2084
+ const countTokensTimeoutMs = opts?.countTokensTimeoutMs ?? XAI_COUNT_TOKENS_TIMEOUT_MS;
2085
+ if (!Number.isInteger(countTokensTimeoutMs) || countTokensTimeoutMs < 1 || countTokensTimeoutMs > XAI_MAX_TIMEOUT_MS) {
2086
+ throw new core.LlmError(
2087
+ `xaiAdapter: countTokensTimeoutMs must be an integer from 1 to ${XAI_MAX_TIMEOUT_MS}.`,
2088
+ { kind: "bad_request", retryable: false, provider: "xai" }
2089
+ );
2090
+ }
916
2091
  return {
917
2092
  id: "xai",
918
2093
  async run(req, ctx) {
@@ -924,12 +2099,16 @@ function xaiAdapter(opts) {
924
2099
  }
925
2100
  const warnings = [];
926
2101
  const model = req.model;
927
- if (req.modelDescriptor !== void 0 && (req.modelDescriptor.model !== model || req.modelDescriptor.provider !== "xai")) {
928
- throw badXaiRequest(`Mismatched xAI model descriptor for "${model}".`);
2102
+ if (req.modelDescriptor !== void 0) {
2103
+ core.assertModelMatchesDescriptor(req, req.modelDescriptor, "xai");
2104
+ }
2105
+ const mediaDescriptor = req.modelDescriptor ?? xaiRegistry.resolve("xai", model);
2106
+ if (mediaDescriptor !== void 0) {
2107
+ core.assertInputMimeTypesAdmitted(req.messages, mediaDescriptor, "xai");
929
2108
  }
930
- if (xaiRegistry.resolve("xai", model)?.capabilities?.statelessReasoningReplay === true && req.modelDescriptor?.capabilities?.statelessReasoningReplay !== true) {
2109
+ if (xaiRegistry.resolve("xai", model)?.capabilities?.continuation === "state" && req.modelDescriptor?.capabilities?.continuation !== "state") {
931
2110
  throw badXaiRequest(
932
- `A matching xAI model descriptor with statelessReasoningReplay is required for "${model}".`
2111
+ `A matching xAI model descriptor with continuation "state" is required for "${model}".`
933
2112
  );
934
2113
  }
935
2114
  const genConfig = req.config;
@@ -937,11 +2116,11 @@ function xaiAdapter(opts) {
937
2116
  genConfig.providerOptions?.["xai"],
938
2117
  model
939
2118
  );
940
- const replayRequired = req.modelDescriptor?.capabilities?.statelessReasoningReplay === true;
2119
+ const replayRequired = req.modelDescriptor?.capabilities?.continuation === "state";
941
2120
  const replayState = parseXaiReplayState(req.transientProviderState, model);
942
2121
  if (replayState !== void 0 && !replayRequired) {
943
2122
  throw badXaiRequest(
944
- `transientProviderState requires a statelessReasoningReplay model descriptor for "${model}".`
2123
+ `transientProviderState requires a model descriptor with continuation "state" for "${model}".`
945
2124
  );
946
2125
  }
947
2126
  if (replayState !== void 0 && req.messages.length === 0) {
@@ -949,12 +2128,12 @@ function xaiAdapter(opts) {
949
2128
  `Stateless conversation replay for model "${model}" requires new messages to append.`
950
2129
  );
951
2130
  }
952
- const input = [...replayState?.input ?? []];
2131
+ const input = [...replayState?.xai.input ?? []];
953
2132
  const replayCallIds = new Set(
954
- replayState?.input.filter((item) => isPlainRecord(item) && item["type"] === "function_call").map((item) => isPlainRecord(item) ? item["call_id"] : void 0).filter((id) => typeof id === "string") ?? []
2133
+ replayState?.xai.input.filter((item) => isPlainRecord(item) && item["type"] === "function_call").map((item) => isPlainRecord(item) ? item["call_id"] : void 0).filter((id) => typeof id === "string") ?? []
955
2134
  );
956
2135
  const replayedResultIds = new Set(
957
- replayState?.input.filter(
2136
+ replayState?.xai.input.filter(
958
2137
  (item) => isPlainRecord(item) && item["type"] === "function_call_output"
959
2138
  ).map((item) => isPlainRecord(item) ? item["call_id"] : void 0).filter((id) => typeof id === "string") ?? []
960
2139
  );
@@ -1074,8 +2253,18 @@ function xaiAdapter(opts) {
1074
2253
  const structuredOutputRequested = req.outputJsonSchema !== void 0;
1075
2254
  if (structuredOutputRequested) {
1076
2255
  const schema = req.outputJsonSchema;
1077
- assertXaiOutputJsonSchema(schema);
1078
- const name = isPlainRecord(schema) && typeof schema["title"] === "string" && schema["title"].length > 0 ? schema["title"] : "structured_output";
2256
+ core.assertJsonSchemaProfile(
2257
+ schema,
2258
+ "output.jsonSchema",
2259
+ XAI_JSON_SCHEMA_PROFILE
2260
+ );
2261
+ const title = isPlainRecord(schema) ? schema["title"] : void 0;
2262
+ if (typeof title === "string" && !XAI_SCHEMA_NAME.test(title)) {
2263
+ throw badXaiRequest(
2264
+ `output.jsonSchema.title "${boundedNote(title)}" cannot be the xAI structured-output name for model "${model}": it must match ${XAI_SCHEMA_NAME.source} (letters, digits, "_" and "-", 1 to 64 characters). Rename the title or remove it to use "${XAI_DEFAULT_SCHEMA_NAME}".`
2265
+ );
2266
+ }
2267
+ const name = typeof title === "string" ? title : XAI_DEFAULT_SCHEMA_NAME;
1079
2268
  params.text = {
1080
2269
  format: { type: "json_schema", name, schema, strict: true }
1081
2270
  };
@@ -1129,6 +2318,13 @@ function xaiAdapter(opts) {
1129
2318
  `tools is not supported for xai model "${model}" (capabilities.functionCalling is not true).`
1130
2319
  );
1131
2320
  }
2321
+ req.tools.forEach((tool, index) => {
2322
+ core.assertJsonSchemaProfile(
2323
+ tool.inputJsonSchema,
2324
+ `tools[${index}].inputJsonSchema`,
2325
+ XAI_JSON_SCHEMA_PROFILE
2326
+ );
2327
+ });
1132
2328
  const functionTools = req.tools.map((tool) => ({
1133
2329
  type: "function",
1134
2330
  name: tool.name,
@@ -1141,130 +2337,300 @@ function xaiAdapter(opts) {
1141
2337
  }
1142
2338
  }
1143
2339
  if (xaiProviderConfig.parallelToolCalls !== void 0) {
2340
+ if (params.tools === void 0 || params.tools.length === 0) {
2341
+ throw badXaiRequest(
2342
+ `providerOptions.xai.parallelToolCalls requires at least one tool (function tools or providerOptions.xai.tools) for model "${model}".`
2343
+ );
2344
+ }
1144
2345
  params.parallel_tool_calls = xaiProviderConfig.parallelToolCalls;
1145
2346
  }
1146
2347
  let response;
2348
+ let responseMeta;
2349
+ let sdkCallStart;
1147
2350
  try {
1148
- const buildClient = opts?._clientFactory ?? buildXaiClient;
1149
- const client = opts?.client !== void 0 ? opts.client : await buildClient(ctx.auth);
2351
+ const buildClient = seams.clientFactory ?? buildXaiClient;
2352
+ const client = opts?.client !== void 0 ? opts.client : await buildClient(ctx.auth, transport);
1150
2353
  ctx.logger.debug(
1151
2354
  { model, configKeys: Object.keys(params) },
1152
2355
  "llm.adapter.dispatch"
1153
2356
  );
1154
- response = await client.responses.create(
1155
- params,
1156
- ctx.signal !== void 0 ? { signal: ctx.signal } : void 0
1157
- );
2357
+ const sdkTimeoutMs = genConfig.timeoutMs !== void 0 ? genConfig.timeoutMs + XAI_TIMEOUT_BUFFER_MS : XAI_DEFAULT_TIMEOUT_MS;
2358
+ const requestOptions = {
2359
+ ...ctx.signal !== void 0 ? { signal: ctx.signal } : {},
2360
+ timeout: sdkTimeoutMs,
2361
+ onResponse: (meta) => {
2362
+ responseMeta = meta;
2363
+ }
2364
+ };
2365
+ sdkCallStart = { startedAt: performance.now(), timeoutMs: sdkTimeoutMs };
2366
+ response = await client.responses.create(params, requestOptions);
1158
2367
  } catch (rawErr) {
1159
- throw classifyXaiError(rawErr);
2368
+ throw classifyXaiError(
2369
+ rawErr,
2370
+ sdkCallStart !== void 0 ? {
2371
+ timeoutMs: sdkCallStart.timeoutMs,
2372
+ elapsedMs: performance.now() - sdkCallStart.startedAt
2373
+ } : void 0,
2374
+ estimateWireInputTokens(params),
2375
+ genConfig.timeoutMs ?? XAI_DEFAULT_TIMEOUT_MS
2376
+ );
2377
+ }
2378
+ if (response.status === "failed" || response.status === "cancelled") {
2379
+ throw failedResponseError(response, responseMeta?.streamProgressed === true);
2380
+ }
2381
+ if (!isPlainRecord(response.usage) || typeof response.usage.input_tokens !== "number" || typeof response.usage.output_tokens !== "number") {
2382
+ throw new core.LlmError(
2383
+ `xAI response ${String(response.id)} (status "${String(response.status)}") carries no numeric usage; it cannot be priced.`,
2384
+ {
2385
+ kind: "server",
2386
+ retryable: false,
2387
+ provider: "xai",
2388
+ cause: { id: response.id }
2389
+ }
2390
+ );
2391
+ }
2392
+ for (const message of responseMeta?.streamNotes ?? []) {
2393
+ warnings.push({ type: "other", message });
1160
2394
  }
1161
- let text = "";
1162
- let reasoningText;
1163
- const messageItems = [];
1164
- const toolCalls = [];
1165
- for (const item of response.output) {
1166
- if (isXaiMessageItem(item)) {
1167
- messageItems.push(item);
1168
- } else if (isXaiReasoningItem(item)) {
1169
- const joined = item.summary.map((s) => s.text).join("");
1170
- if (joined.length > 0) {
1171
- reasoningText = (reasoningText ?? "") + joined;
2395
+ try {
2396
+ let text = "";
2397
+ const reasoningChunks = [];
2398
+ const messageItems = [];
2399
+ const toolCalls = [];
2400
+ const droppedCalls = [];
2401
+ const droppedItems = /* @__PURE__ */ new Set();
2402
+ const outputOrder = [];
2403
+ for (const item of response.output) {
2404
+ if (isXaiMessageItem(item)) {
2405
+ messageItems.push(item);
2406
+ outputOrder.push(item);
2407
+ } else if (isXaiReasoningItem(item)) {
2408
+ for (const part of item.summary) {
2409
+ const summary = partText(part);
2410
+ if (summary !== void 0 && summary.length > 0) {
2411
+ reasoningChunks.push(summary);
2412
+ }
2413
+ }
2414
+ } else if (item.type === "function_call") {
2415
+ const callId = typeof item["call_id"] === "string" ? item["call_id"] : "";
2416
+ const name = typeof item["name"] === "string" ? item["name"] : "";
2417
+ if (response.status !== "completed" || typeof item["status"] === "string" && item["status"] !== "completed") {
2418
+ droppedCalls.push(name.length > 0 ? name : callId);
2419
+ droppedItems.add(item);
2420
+ continue;
2421
+ }
2422
+ let args = {};
2423
+ if (typeof item["arguments"] === "string") {
2424
+ try {
2425
+ args = JSON.parse(item["arguments"]);
2426
+ } catch (cause) {
2427
+ throw new core.LlmError(
2428
+ `xAI response ${String(response.id)} holds a completed function call "${name}" whose arguments are not valid JSON.`,
2429
+ {
2430
+ kind: "server",
2431
+ retryable: false,
2432
+ provider: "xai",
2433
+ usage: mapUsage(response.usage),
2434
+ ...typeof response.service_tier === "string" && response.service_tier.length > 0 ? { servedServiceTier: response.service_tier } : {},
2435
+ cause
2436
+ }
2437
+ );
2438
+ }
2439
+ }
2440
+ if (callId.length > 0 && name.length > 0) {
2441
+ const call = { toolCallId: callId, toolName: name, args };
2442
+ toolCalls.push(call);
2443
+ outputOrder.push(call);
2444
+ }
1172
2445
  }
1173
- } else if (item.type === "function_call") {
1174
- const callId = typeof item["call_id"] === "string" ? item["call_id"] : "";
1175
- const name = typeof item["name"] === "string" ? item["name"] : "";
1176
- let args = {};
1177
- if (typeof item["arguments"] === "string") {
1178
- try {
1179
- args = JSON.parse(item["arguments"]);
1180
- } catch {
1181
- args = item["arguments"];
2446
+ }
2447
+ const reasoningText = reasoningChunks.length > 0 ? reasoningChunks.join("\n\n") : void 0;
2448
+ let refusal;
2449
+ if (messageItems.length > 0) {
2450
+ const lastMessage = messageItems[messageItems.length - 1];
2451
+ const ignoredTypes = /* @__PURE__ */ new Map();
2452
+ for (const part of lastMessage.content) {
2453
+ const own = partText(part);
2454
+ if (own !== void 0) {
2455
+ text += own;
2456
+ } else if (isPlainRecord(part) && part["type"] === "refusal") {
2457
+ const said = typeof part["refusal"] === "string" ? part["refusal"] : "";
2458
+ refusal = `${refusal === void 0 ? "" : `${refusal} `}${said}`;
2459
+ } else {
2460
+ const type = isPlainRecord(part) && typeof part["type"] === "string" ? part["type"] : "unknown";
2461
+ ignoredTypes.set(type, (ignoredTypes.get(type) ?? 0) + 1);
1182
2462
  }
1183
2463
  }
1184
- if (callId.length > 0 && name.length > 0) {
1185
- toolCalls.push({ toolCallId: callId, toolName: name, args });
2464
+ for (const [type, count] of ignoredTypes) {
2465
+ warnings.push({
2466
+ type: "other",
2467
+ message: `xai: ignored ${count} message content part(s) of type "${type}" that carry no text; the answer text is built from the output_text parts.`
2468
+ });
2469
+ }
2470
+ if (messageItems.length > 1) {
2471
+ warnings.push({
2472
+ type: "other",
2473
+ message: `xai: response contained ${messageItems.length} message output items; using the last one and discarding ${messageItems.length - 1} earlier message item(s).`
2474
+ });
1186
2475
  }
1187
2476
  }
1188
- }
1189
- if (messageItems.length > 0) {
1190
- const lastMessage = messageItems[messageItems.length - 1];
1191
- text = lastMessage.content.map((part) => part.text).join("");
1192
- if (messageItems.length > 1) {
2477
+ const lastMessageItem = messageItems[messageItems.length - 1];
2478
+ const messageParts = [];
2479
+ for (const entry of outputOrder) {
2480
+ if ("toolCallId" in entry) {
2481
+ messageParts.push({ kind: "tool-call", ...entry });
2482
+ } else if (entry === lastMessageItem && text.length > 0) {
2483
+ messageParts.push({ kind: "text", text });
2484
+ }
2485
+ }
2486
+ let rawStructured;
2487
+ if (structuredOutputRequested && text.length > 0) {
2488
+ try {
2489
+ rawStructured = JSON.parse(text);
2490
+ } catch {
2491
+ }
2492
+ }
2493
+ const usage = mapUsage(response.usage);
2494
+ let finishReason = mapFinishReason(response);
2495
+ if (refusal !== void 0) {
2496
+ if (finishReason !== "length") finishReason = "content_filter";
1193
2497
  warnings.push({
1194
2498
  type: "other",
1195
- message: `xai: response contained ${messageItems.length} message output items; using the last one and discarding ${messageItems.length - 1} earlier message item(s).`
2499
+ message: `xai: the model refused to answer${refusal.length > 0 ? `: "${boundedNote(refusal)}"` : ""}; the result carries no text for it and finishReason is "${finishReason}".`
1196
2500
  });
1197
2501
  }
1198
- }
1199
- let rawStructured;
1200
- if (structuredOutputRequested && text.length > 0) {
1201
- try {
1202
- rawStructured = JSON.parse(text);
1203
- } catch {
2502
+ if (droppedCalls.length > 0) {
2503
+ const named = droppedCalls.map((name) => `"${name}"`).join(", ");
2504
+ const reason = response.incomplete_details?.reason;
2505
+ warnings.push({
2506
+ type: "other",
2507
+ message: `xai: dropped ${droppedCalls.length} function call(s) (${named}) because the response ended with status "${String(response.status)}"${typeof reason === "string" ? ` (${reason})` : ""}, not "completed": ${reason === "max_output_tokens" ? "the call was cut by the output cap and its arguments are incomplete" : "a call beside an abnormal end is not a call to run"}. The result carries no tool call; finishReason is "${finishReason}".`
2508
+ });
1204
2509
  }
1205
- }
1206
- const usage = mapUsage(response.usage);
1207
- const finishReason = mapFinishReason(response);
1208
- const expectedToolCounters = expectedServerToolCounters(
1209
- xaiProviderConfig.tools);
1210
- const noServerToolRan = response.usage["num_server_side_tools_used"] === 0 && response.usage["server_side_tool_usage_details"] === void 0;
1211
- if ((expectedToolCounters.length > 0 || hasFileRef) && !noServerToolRan) {
1212
- usage.details["server_tools_requested"] = 1;
1213
- if (xaiProviderConfig.tools?.some((tool) => tool["type"] === "x_search") === true) {
1214
- usage.details["x_search_requested"] = 1;
2510
+ const expectedToolCounters = expectedServerToolCounters(xaiProviderConfig.tools);
2511
+ const noServerToolRan = response.usage["num_server_side_tools_used"] === 0 && response.usage["server_side_tool_usage_details"] === void 0;
2512
+ if (xaiProviderConfig.tools?.some((tool) => tool["type"] === "web_search") === true) {
2513
+ usage.details["web_search_requested"] = 1;
2514
+ if (noServerToolRan) usage.details[WEB_SEARCH_COUNTER] = 0;
2515
+ }
2516
+ if (xaiProviderConfig.searchBudget !== void 0) {
2517
+ const over = exceededSearchBudget(xaiProviderConfig.searchBudget, usage.details);
2518
+ if (over.length > 0) {
2519
+ usage.details["search_budget_exceeded"] = 1;
2520
+ warnings.push({
2521
+ type: "other",
2522
+ message: `xai: search budget exceeded (${over.join("; ")}); the call is already billed and its result is returned.`
2523
+ });
2524
+ }
1215
2525
  }
1216
- const missing = expectedToolCounters.filter((key) => !(key in usage.details));
1217
- if (missing.length > 0) {
1218
- usage.details["server_tools_missing"] = 1;
2526
+ const { feeUnpriced, unknown } = classifyUnpricedXaiToolCounters(usage);
2527
+ const shown = (keys) => keys.map((key) => `${key}=${String(usage.details[key])}`).join(", ");
2528
+ const feeCounters = hasFileRef ? feeUnpriced.filter((key) => key !== "document_search_calls") : feeUnpriced;
2529
+ if (feeCounters.length > 0) {
1219
2530
  warnings.push({
1220
2531
  type: "other",
1221
- message: `xai: server tools were requested but usage is missing counters [${missing.join(
1222
- ", "
1223
- )}]; the call is unpriced.`
2532
+ message: `xai: server tool counter(s) [${shown(feeCounters)}] are non-zero but have no rate in the pricing snapshot (xAI bills the tool per use); the call's cost is estimated and understates.`
1224
2533
  });
1225
2534
  }
1226
- if (hasFileRef) {
1227
- usage.details["attachment_search_unpinned"] = 1;
2535
+ if (unknown.length > 0) {
2536
+ const understanding = xaiProviderConfig.tools?.some(
2537
+ (tool) => tool["enable_image_understanding"] === true || tool["enable_video_understanding"] === true
2538
+ ) === true;
1228
2539
  warnings.push({
1229
2540
  type: "other",
1230
- message: "xai: file-ref enables attachment_search but that counter is not live-pinned; tool cost is estimated."
2541
+ message: understanding ? `xai: server tool counter(s) [${shown(unknown)}] are not in the pricing snapshot. The request enabled image or video understanding, which xAI prices by tokens with no invocation fee, so the counter is probably that tool's and the priced token cost may be complete; the counter's name was never captured, so the call stays estimated.` : `xai: server tool counter(s) [${shown(unknown)}] are not in the pricing snapshot; xAI may bill the tool per use, so the call's cost is estimated and may understate.`
1231
2542
  });
1232
2543
  }
1233
- }
1234
- const citations = collectXaiCitations(response, messageItems);
1235
- const providerMeta = {};
1236
- const contextDetails = response.usage["context_details"];
1237
- if (isPlainRecord(contextDetails)) {
1238
- providerMeta["context_details"] = contextDetails;
1239
- }
1240
- if (isPlainRecord(response.metadata)) {
1241
- providerMeta["metadata"] = response.metadata;
1242
- }
1243
- let transientProviderState;
1244
- if (replayRequired) {
1245
- const state = {
1246
- model,
1247
- input: [...params.input, ...response.output]
2544
+ if ((expectedToolCounters.length > 0 || hasFileRef) && !noServerToolRan) {
2545
+ usage.details["server_tools_requested"] = 1;
2546
+ if (xaiProviderConfig.tools?.some((tool) => tool["type"] === "x_search") === true) {
2547
+ usage.details["x_search_requested"] = 1;
2548
+ }
2549
+ const missing = expectedToolCounters.filter((key) => !(key in usage.details));
2550
+ if (missing.length > 0) {
2551
+ usage.details["server_tools_missing"] = 1;
2552
+ warnings.push({
2553
+ type: "other",
2554
+ message: `xai: server tools were requested but usage is missing counters [${missing.join(
2555
+ ", "
2556
+ )}]; the call is unpriced.`
2557
+ });
2558
+ }
2559
+ if (hasFileRef) {
2560
+ usage.details["attachment_search_unpinned"] = 1;
2561
+ warnings.push({
2562
+ type: "other",
2563
+ message: "xai: file-ref enables attachment_search but that counter is not live-pinned; tool cost is estimated."
2564
+ });
2565
+ }
2566
+ }
2567
+ const citations = collectXaiCitations(
2568
+ response,
2569
+ messageItems,
2570
+ (message) => warnings.push({ type: "other", message })
2571
+ );
2572
+ const providerMeta = {};
2573
+ const contextDetails = response.usage["context_details"];
2574
+ if (isPlainRecord(contextDetails)) {
2575
+ providerMeta["context_details"] = contextDetails;
2576
+ }
2577
+ if (isPlainRecord(response.metadata)) {
2578
+ providerMeta["metadata"] = response.metadata;
2579
+ }
2580
+ if (responseMeta !== void 0) {
2581
+ const xaiMeta = {};
2582
+ if (responseMeta.requestId !== void 0)
2583
+ xaiMeta["requestId"] = responseMeta.requestId;
2584
+ if (responseMeta.rateLimitRemaining !== void 0) {
2585
+ xaiMeta["rateLimitRemaining"] = { ...responseMeta.rateLimitRemaining };
2586
+ }
2587
+ if (Object.keys(xaiMeta).length > 0) providerMeta["xai"] = xaiMeta;
2588
+ }
2589
+ let transientProviderState;
2590
+ if (replayRequired) {
2591
+ const state = {
2592
+ xai: {
2593
+ model,
2594
+ input: [
2595
+ ...params.input,
2596
+ ...response.output.filter((item) => !droppedItems.has(item))
2597
+ ]
2598
+ }
2599
+ };
2600
+ transientProviderState = state;
2601
+ }
2602
+ const servedServiceTier = typeof response.service_tier === "string" && response.service_tier.length > 0 ? response.service_tier : void 0;
2603
+ const result = {
2604
+ model: response.model,
2605
+ message: { role: "assistant", parts: messageParts },
2606
+ usage,
2607
+ warnings,
2608
+ finishReason,
2609
+ responseId: response.id,
2610
+ ...text.length > 0 ? { text } : {},
2611
+ ...reasoningText !== void 0 ? { reasoningText } : {},
2612
+ ...rawStructured !== void 0 ? { rawStructured } : {},
2613
+ ...servedServiceTier !== void 0 ? { servedServiceTier } : {},
2614
+ ...Object.keys(providerMeta).length > 0 ? { providerMetadata: providerMeta } : {},
2615
+ ...transientProviderState !== void 0 ? { transientProviderState } : {},
2616
+ ...citations.length > 0 ? { citations } : {},
2617
+ ...toolCalls.length > 0 ? { toolCalls, finishReason: "tool_calls" } : {}
1248
2618
  };
1249
- transientProviderState = state;
1250
- }
1251
- const servedServiceTier = typeof response.service_tier === "string" && response.service_tier.length > 0 ? response.service_tier : void 0;
1252
- const result = {
1253
- model: response.model,
1254
- usage,
1255
- warnings,
1256
- finishReason,
1257
- responseId: response.id,
1258
- ...text.length > 0 ? { text } : {},
1259
- ...reasoningText !== void 0 ? { reasoningText } : {},
1260
- ...rawStructured !== void 0 ? { rawStructured } : {},
1261
- ...servedServiceTier !== void 0 ? { servedServiceTier } : {},
1262
- ...Object.keys(providerMeta).length > 0 ? { providerMetadata: providerMeta } : {},
1263
- ...transientProviderState !== void 0 ? { transientProviderState } : {},
1264
- ...citations.length > 0 ? { citations } : {},
1265
- ...toolCalls.length > 0 ? { toolCalls, finishReason: "tool_calls" } : {}
1266
- };
1267
- return result;
2619
+ return result;
2620
+ } catch (mapErr) {
2621
+ if (mapErr instanceof core.LlmError) throw mapErr;
2622
+ throw new core.LlmError(
2623
+ `xAI response ${String(response.id)} could not be mapped: ${mapErr instanceof Error ? mapErr.message : String(mapErr)}`,
2624
+ {
2625
+ kind: "server",
2626
+ retryable: false,
2627
+ provider: "xai",
2628
+ usage: mapUsage(response.usage),
2629
+ ...typeof response.service_tier === "string" && response.service_tier.length > 0 ? { servedServiceTier: response.service_tier } : {},
2630
+ cause: mapErr
2631
+ }
2632
+ );
2633
+ }
1268
2634
  },
1269
2635
  async countTokens(req, ctx) {
1270
2636
  if (req.provider !== "xai") {
@@ -1280,16 +2646,32 @@ function xaiAdapter(opts) {
1280
2646
  }
1281
2647
  const text = concatenateTokenizeText(req);
1282
2648
  const apiKey = requireApiKey(ctx.auth);
1283
- const fetchImpl = opts?._fetch ?? fetch;
2649
+ const fetchImpl = seams.fetch ?? transport?.fetch ?? fetch;
2650
+ const controller = new AbortController();
2651
+ const forwardAbort = () => {
2652
+ controller.abort(ctx.signal?.reason);
2653
+ };
2654
+ if (ctx.signal?.aborted === true) forwardAbort();
2655
+ else ctx.signal?.addEventListener("abort", forwardAbort, { once: true });
2656
+ const timeout = new core.LlmError(
2657
+ `xAI tokenize-text timed out after ${countTokensTimeoutMs}ms`,
2658
+ { kind: "timeout", retryable: true, provider: "xai" }
2659
+ );
2660
+ const timer = setTimeout(() => {
2661
+ controller.abort(timeout);
2662
+ }, countTokensTimeoutMs);
1284
2663
  try {
1285
2664
  const res = await fetchImpl("https://api.x.ai/v1/tokenize-text", {
2665
+ // The host transport's init (a dispatcher, say) first; the request
2666
+ // owns the keys below, which `snapshotXaiTransport` keeps out of it.
2667
+ ...seams.fetch === void 0 ? transport?.fetchOptions : void 0,
1286
2668
  method: "POST",
1287
2669
  headers: {
1288
2670
  Authorization: `Bearer ${apiKey}`,
1289
2671
  "Content-Type": "application/json"
1290
2672
  },
1291
2673
  body: JSON.stringify({ model: req.model, text }),
1292
- ...ctx.signal !== void 0 ? { signal: ctx.signal } : {}
2674
+ signal: controller.signal
1293
2675
  });
1294
2676
  if (!res.ok) {
1295
2677
  let parsed;
@@ -1298,12 +2680,26 @@ function xaiAdapter(opts) {
1298
2680
  } catch {
1299
2681
  parsed = await res.text().catch(() => "");
1300
2682
  }
1301
- throw Object.assign(new Error(`xAI tokenize-text HTTP ${res.status}`), {
1302
- status: res.status,
1303
- error: parsed
2683
+ const requestId = res.headers.get("x-request-id");
2684
+ throw Object.assign(
2685
+ new Error(
2686
+ `xAI tokenize-text HTTP ${res.status}${requestId !== null && requestId !== "" ? ` (request id ${requestId})` : ""}`
2687
+ ),
2688
+ { status: res.status, error: parsed, headers: res.headers }
2689
+ );
2690
+ }
2691
+ let raw;
2692
+ try {
2693
+ raw = await res.json();
2694
+ } catch (cause) {
2695
+ if (controller.signal.aborted) throw cause;
2696
+ throw new core.LlmError("xAI tokenize-text response is not JSON", {
2697
+ kind: "server",
2698
+ retryable: true,
2699
+ provider: "xai",
2700
+ cause
1304
2701
  });
1305
2702
  }
1306
- const raw = await res.json();
1307
2703
  if (!isPlainRecord(raw) || !Array.isArray(raw["token_ids"])) {
1308
2704
  throw new core.LlmError(
1309
2705
  "xAI tokenize-text response is malformed: missing required field: token_ids",
@@ -1318,6 +2714,9 @@ function xaiAdapter(opts) {
1318
2714
  raw
1319
2715
  };
1320
2716
  } catch (rawErr) {
2717
+ if (controller.signal.aborted && controller.signal.reason === timeout) {
2718
+ throw timeout;
2719
+ }
1321
2720
  if (rawErr instanceof Error && rawErr.name === "AbortError") {
1322
2721
  throw new core.LlmError("xAI tokenize-text aborted", {
1323
2722
  kind: "aborted",
@@ -1327,12 +2726,15 @@ function xaiAdapter(opts) {
1327
2726
  });
1328
2727
  }
1329
2728
  throw classifyXaiError(rawErr);
2729
+ } finally {
2730
+ clearTimeout(timer);
2731
+ ctx.signal?.removeEventListener("abort", forwardAbort);
1330
2732
  }
1331
2733
  }
1332
2734
  };
1333
2735
  }
1334
2736
  var WEB_SEARCH_COUNTER = "web_search_calls";
1335
- function expectedServerToolCounters(tools, _hasFileRef) {
2737
+ function expectedServerToolCounters(tools) {
1336
2738
  const keys = [];
1337
2739
  if (tools !== void 0) {
1338
2740
  for (const tool of tools) {
@@ -1342,49 +2744,71 @@ function expectedServerToolCounters(tools, _hasFileRef) {
1342
2744
  }
1343
2745
  return keys;
1344
2746
  }
1345
- function collectXaiCitations(response, messageItems) {
1346
- const seen = /* @__PURE__ */ new Set();
1347
- const citations = [];
1348
- const push = (url, title) => {
1349
- if (typeof url !== "string" || url.length === 0) return;
1350
- if (seen.has(url)) return;
1351
- seen.add(url);
1352
- const citation = { url };
1353
- if (typeof title === "string" && title.length > 0 && title !== url) {
1354
- citation.title = title;
1355
- }
1356
- try {
1357
- const parsed = new URL(url);
1358
- if (parsed.hostname.length > 0) {
1359
- citation.sourceName = parsed.hostname.startsWith("www.") ? parsed.hostname.slice(4) : parsed.hostname;
2747
+ function inlineMarkerLabel(marker, url) {
2748
+ const tail = `]](${url})`;
2749
+ return marker.startsWith("[[") && marker.endsWith(tail) && marker.length >= 2 + tail.length ? marker.slice(2, marker.length - tail.length) : void 0;
2750
+ }
2751
+ function collectXaiCitations(response, messageItems, onDropped) {
2752
+ const byUrl = /* @__PURE__ */ new Map();
2753
+ const upsert = (url, title, markerLabel) => {
2754
+ if (typeof url !== "string" || url.length === 0) return void 0;
2755
+ let citation = byUrl.get(url);
2756
+ if (citation === void 0) {
2757
+ citation = { url };
2758
+ try {
2759
+ const parsed = new URL(url);
2760
+ if (parsed.hostname.length > 0) {
2761
+ citation.sourceName = parsed.hostname.startsWith("www.") ? parsed.hostname.slice(4) : parsed.hostname;
2762
+ }
2763
+ } catch {
1360
2764
  }
1361
- } catch {
2765
+ byUrl.set(url, citation);
1362
2766
  }
1363
- citations.push(citation);
2767
+ if (citation.title === void 0 && typeof title === "string" && title.length > 0 && title !== url && title !== markerLabel) {
2768
+ citation.title = title;
2769
+ }
2770
+ return citation;
1364
2771
  };
1365
2772
  if (Array.isArray(response.citations)) {
1366
2773
  for (const item of response.citations) {
1367
2774
  if (typeof item === "string") {
1368
- push(item, void 0);
2775
+ upsert(item, void 0);
1369
2776
  } else if (isPlainRecord(item)) {
1370
- push(item["url"] ?? item["uri"], item["title"]);
2777
+ upsert(item["url"] ?? item["uri"], item["title"]);
1371
2778
  }
1372
2779
  }
1373
2780
  }
1374
2781
  const lastMessage = messageItems.at(-1);
1375
- const citationMessages = lastMessage === void 0 ? [] : [lastMessage];
1376
- for (const item of citationMessages) {
1377
- for (const part of item.content) {
1378
- const annotations = part.annotations;
1379
- if (!Array.isArray(annotations)) continue;
1380
- for (const ann of annotations) {
1381
- if (!isPlainRecord(ann)) continue;
1382
- if (ann["type"] !== void 0 && ann["type"] !== "url_citation") continue;
1383
- push(ann["url"], ann["title"]);
2782
+ if (lastMessage !== void 0) {
2783
+ const joined = lastMessage.content.map((part) => partText(part) ?? "").join("");
2784
+ let partOffset = 0;
2785
+ for (const part of lastMessage.content) {
2786
+ const annotations = isPlainRecord(part) ? part["annotations"] : void 0;
2787
+ if (Array.isArray(annotations)) {
2788
+ for (const ann of annotations) {
2789
+ if (!isPlainRecord(ann)) continue;
2790
+ if (ann["type"] !== void 0 && ann["type"] !== "url_citation") continue;
2791
+ const start = ann["start_index"];
2792
+ const end = ann["end_index"];
2793
+ const hasRange = typeof start === "number" && typeof end === "number" && Number.isInteger(start) && Number.isInteger(end) && start >= 0 && end > start;
2794
+ const url = ann["url"];
2795
+ const label = hasRange && typeof url === "string" ? inlineMarkerLabel(joined.slice(partOffset + start, partOffset + end), url) : void 0;
2796
+ const citation = upsert(url, ann["title"], label);
2797
+ if (citation === void 0 || !hasRange) continue;
2798
+ citation.cited = true;
2799
+ if (label === void 0) {
2800
+ onDropped(
2801
+ `xai: dropped a textRange for a citation of ${citation.url}: the answer at start_index ${start}, end_index ${end} is not the inline [[N]](url) marker. The source stays cited without a range.`
2802
+ );
2803
+ } else {
2804
+ citation.textRange ??= { start: partOffset + start, end: partOffset + end };
2805
+ }
2806
+ }
1384
2807
  }
2808
+ partOffset += (partText(part) ?? "").length;
1385
2809
  }
1386
2810
  }
1387
- return citations;
2811
+ return [...byUrl.values()];
1388
2812
  }
1389
2813
  function concatenateTokenizeText(req) {
1390
2814
  const chunks = [];
@@ -1416,6 +2840,7 @@ var XAI_FILE_TTL_MIN_SECONDS = 3600;
1416
2840
  var XAI_FILE_TTL_MAX_SECONDS = 2592e3;
1417
2841
  var XAI_FILE_MAX_BYTES = 48 * 1024 * 1024;
1418
2842
  var XAI_FILES_DEFAULT_BASE_URL = "https://api.x.ai/v1";
2843
+ var XAI_FILES_DEFAULT_TIMEOUT_MS = 6e4;
1419
2844
  function isPlainRecord2(value) {
1420
2845
  return typeof value === "object" && value !== null && !Array.isArray(value);
1421
2846
  }
@@ -1484,19 +2909,23 @@ function toBlob(data, mimeType) {
1484
2909
  var XaiFilesHttpError = class extends Error {
1485
2910
  status;
1486
2911
  error;
1487
- constructor(status, message, errorBody) {
2912
+ /** The response headers: `classifyError` reads `Retry-After` from them. */
2913
+ headers;
2914
+ constructor(status, message, errorBody, headers) {
1488
2915
  super(message);
1489
2916
  this.name = "XaiFilesHttpError";
1490
2917
  this.status = status;
1491
2918
  this.error = errorBody;
2919
+ this.headers = headers;
1492
2920
  }
1493
2921
  };
1494
- async function throwHttpFailure(res) {
2922
+ async function throwHttpFailure(res, signal, operation) {
1495
2923
  const status = res.status;
1496
2924
  let bodyText = "";
1497
2925
  try {
1498
2926
  bodyText = await res.text();
1499
- } catch {
2927
+ } catch (e) {
2928
+ if (signal.aborted) throw abortedError(signal, `xAI file ${operation} aborted`, e);
1500
2929
  bodyText = "";
1501
2930
  }
1502
2931
  let parsed = bodyText;
@@ -1513,7 +2942,9 @@ async function throwHttpFailure(res) {
1513
2942
  } else if (isPlainRecord2(parsed) && typeof parsed["error"] === "string") {
1514
2943
  message = parsed["error"];
1515
2944
  }
1516
- throw new XaiFilesHttpError(status, message, parsed);
2945
+ const requestId = res.headers.get("x-request-id");
2946
+ if (requestId !== null && requestId !== "") message += ` (request id ${requestId})`;
2947
+ throw new XaiFilesHttpError(status, message, parsed, res.headers);
1517
2948
  }
1518
2949
  function isNotFoundError(err) {
1519
2950
  if (err instanceof core.LlmError && err.httpStatus === 404) {
@@ -1582,16 +3013,41 @@ function classifyStoreError(raw) {
1582
3013
  cause: classified.cause ?? raw
1583
3014
  });
1584
3015
  }
3016
+ var DEADLINE_ERRORS = /* @__PURE__ */ new WeakSet();
3017
+ function abortedError(signal, message, cause) {
3018
+ const reason = signal.reason;
3019
+ if (reason instanceof core.LlmError && DEADLINE_ERRORS.has(reason)) return reason;
3020
+ return new core.LlmError(message, {
3021
+ kind: "aborted",
3022
+ retryable: false,
3023
+ provider: "xai",
3024
+ ...cause !== void 0 ? { cause } : {}
3025
+ });
3026
+ }
3027
+ function pathSegment(id) {
3028
+ if (id === "." || id === "..") {
3029
+ throw badRequest(`fileId "${id}" is not a valid file id.`);
3030
+ }
3031
+ return encodeURIComponent(id);
3032
+ }
1585
3033
  var XaiFileStore = class {
1586
3034
  apiKey;
1587
3035
  baseUrl;
1588
3036
  fetchImpl;
3037
+ timeoutMs;
1589
3038
  onDeleteError;
1590
3039
  logger;
1591
3040
  constructor(opts) {
1592
3041
  this.apiKey = requireApiKey(opts.auth);
1593
3042
  this.baseUrl = (opts.baseUrl ?? XAI_FILES_DEFAULT_BASE_URL).replace(/\/+$/, "");
1594
3043
  this.fetchImpl = opts.fetch ?? fetch;
3044
+ const timeoutMs = opts.timeoutMs ?? XAI_FILES_DEFAULT_TIMEOUT_MS;
3045
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > XAI_MAX_TIMEOUT_MS) {
3046
+ throw badRequest(
3047
+ `XaiFileStoreOptions.timeoutMs must be an integer from 1 to ${XAI_MAX_TIMEOUT_MS}.`
3048
+ );
3049
+ }
3050
+ this.timeoutMs = timeoutMs;
1595
3051
  this.logger = opts.logger;
1596
3052
  this.onDeleteError = opts.onDeleteError ?? ((fileId, err) => {
1597
3053
  const message = core.redactSecrets(core.classifyError(err).message);
@@ -1625,6 +3081,37 @@ var XaiFileStore = class {
1625
3081
  }
1626
3082
  return init;
1627
3083
  }
3084
+ /**
3085
+ * Runs one call under the store's deadline: `run` gets a signal that aborts
3086
+ * when the caller's does or when `timeoutMs` passes (headers and body read
3087
+ * both count), and the timer is cleared however the call ends.
3088
+ */
3089
+ async bounded(caller, run) {
3090
+ const controller = new AbortController();
3091
+ const forward = () => {
3092
+ controller.abort(caller?.reason);
3093
+ };
3094
+ if (caller?.aborted === true) forward();
3095
+ else caller?.addEventListener("abort", forward, { once: true });
3096
+ const timer = setTimeout(() => {
3097
+ const error = new core.LlmError(
3098
+ `xAI Files API call timed out after ${this.timeoutMs}ms`,
3099
+ {
3100
+ kind: "timeout",
3101
+ retryable: true,
3102
+ provider: "xai"
3103
+ }
3104
+ );
3105
+ DEADLINE_ERRORS.add(error);
3106
+ controller.abort(error);
3107
+ }, this.timeoutMs);
3108
+ try {
3109
+ return await run(controller.signal);
3110
+ } finally {
3111
+ clearTimeout(timer);
3112
+ caller?.removeEventListener("abort", forward);
3113
+ }
3114
+ }
1628
3115
  /**
1629
3116
  * Upload bytes to xAI Files. Returns immediately with metadata (no poll).
1630
3117
  *
@@ -1656,77 +3143,64 @@ var XaiFileStore = class {
1656
3143
  }
1657
3144
  form.append("purpose", purpose);
1658
3145
  form.append("file", toBlob(input.data, input.mimeType), input.filename);
1659
- let res;
1660
- try {
1661
- res = await this.fetchImpl(
1662
- this.filesUrl(),
1663
- this.requestInit("POST", {
1664
- body: form,
1665
- ...signal !== void 0 ? { signal } : {}
1666
- })
1667
- );
1668
- } catch (e) {
1669
- if (signal?.aborted === true) {
1670
- throw new core.LlmError("xAI file upload aborted", {
1671
- kind: "aborted",
1672
- retryable: false,
1673
- provider: "xai"
1674
- });
1675
- }
1676
- throw classifyStoreError(e);
1677
- }
1678
- if (!res.ok) {
3146
+ return this.bounded(signal, async (bound) => {
3147
+ let res;
1679
3148
  try {
1680
- await throwHttpFailure(res);
3149
+ res = await this.fetchImpl(
3150
+ this.filesUrl(),
3151
+ this.requestInit("POST", { body: form, signal: bound })
3152
+ );
1681
3153
  } catch (e) {
3154
+ if (bound.aborted) throw abortedError(bound, "xAI file upload aborted");
1682
3155
  throw classifyStoreError(e);
1683
3156
  }
1684
- }
1685
- let json;
1686
- try {
1687
- json = await res.json();
1688
- } catch (e) {
1689
- throw new core.LlmError("xAI file upload returned non-JSON body", {
1690
- kind: "server",
1691
- retryable: false,
1692
- provider: "xai",
1693
- cause: e
1694
- });
1695
- }
1696
- return makeHandle(json);
3157
+ if (!res.ok) {
3158
+ try {
3159
+ await throwHttpFailure(res, bound, "upload");
3160
+ } catch (e) {
3161
+ throw classifyStoreError(e);
3162
+ }
3163
+ }
3164
+ let json;
3165
+ try {
3166
+ json = await res.json();
3167
+ } catch (e) {
3168
+ if (bound.aborted) throw abortedError(bound, "xAI file upload aborted", e);
3169
+ throw new core.LlmError("xAI file upload returned non-JSON body", {
3170
+ kind: "server",
3171
+ retryable: false,
3172
+ provider: "xai",
3173
+ cause: e
3174
+ });
3175
+ }
3176
+ return makeHandle(json);
3177
+ });
1697
3178
  }
1698
3179
  async get(fileId, signal) {
1699
3180
  if (typeof fileId !== "string" || fileId.trim() === "") {
1700
3181
  throw badRequest("fileId must be a non-empty string.");
1701
3182
  }
1702
- let res;
1703
- try {
1704
- res = await this.fetchImpl(
1705
- this.filesUrl(fileId),
1706
- this.requestInit("GET", signal !== void 0 ? { signal } : {})
1707
- );
1708
- } catch (e) {
1709
- if (signal?.aborted === true) {
1710
- throw new core.LlmError("xAI file get aborted", {
1711
- kind: "aborted",
1712
- retryable: false,
1713
- provider: "xai"
1714
- });
1715
- }
1716
- throw classifyStoreError(e);
1717
- }
1718
- if (res.status === 404) {
1719
- throw notFoundError(fileId, "get");
1720
- }
1721
- if (!res.ok) {
3183
+ const url = this.filesUrl(pathSegment(fileId));
3184
+ return this.bounded(signal, async (bound) => {
3185
+ let res;
1722
3186
  try {
1723
- await throwHttpFailure(res);
3187
+ res = await this.fetchImpl(url, this.requestInit("GET", { signal: bound }));
1724
3188
  } catch (e) {
3189
+ if (bound.aborted) throw abortedError(bound, "xAI file get aborted");
1725
3190
  throw classifyStoreError(e);
1726
3191
  }
1727
- }
1728
- const json = await res.json();
1729
- return makeHandle(json);
3192
+ if (res.status === 404) {
3193
+ throw notFoundError(fileId, "get");
3194
+ }
3195
+ if (!res.ok) {
3196
+ try {
3197
+ await throwHttpFailure(res, bound, "get");
3198
+ } catch (e) {
3199
+ throw classifyStoreError(e);
3200
+ }
3201
+ }
3202
+ return makeHandle(await readJsonBody(res, bound, "get"));
3203
+ });
1730
3204
  }
1731
3205
  async list(opts = {}, signal) {
1732
3206
  if (opts.limit !== void 0) {
@@ -1743,36 +3217,29 @@ var XaiFileStore = class {
1743
3217
  }
1744
3218
  const qs = params.toString();
1745
3219
  const url = qs.length > 0 ? `${this.filesUrl()}?${qs}` : this.filesUrl();
1746
- let res;
1747
- try {
1748
- res = await this.fetchImpl(
1749
- url,
1750
- this.requestInit("GET", signal !== void 0 ? { signal } : {})
1751
- );
1752
- } catch (e) {
1753
- if (signal?.aborted === true) {
1754
- throw new core.LlmError("xAI file list aborted", {
1755
- kind: "aborted",
1756
- retryable: false,
1757
- provider: "xai"
1758
- });
1759
- }
1760
- throw classifyStoreError(e);
1761
- }
1762
- if (!res.ok) {
3220
+ return this.bounded(signal, async (bound) => {
3221
+ let res;
1763
3222
  try {
1764
- await throwHttpFailure(res);
3223
+ res = await this.fetchImpl(url, this.requestInit("GET", { signal: bound }));
1765
3224
  } catch (e) {
3225
+ if (bound.aborted) throw abortedError(bound, "xAI file list aborted");
1766
3226
  throw classifyStoreError(e);
1767
3227
  }
1768
- }
1769
- const json = await res.json();
1770
- const files = Array.isArray(json.data) ? json.data.map((f) => makeHandle(f)) : [];
1771
- const result = { files };
1772
- if (typeof json.pagination_token === "string" && json.pagination_token.length > 0) {
1773
- result.paginationToken = json.pagination_token;
1774
- }
1775
- return result;
3228
+ if (!res.ok) {
3229
+ try {
3230
+ await throwHttpFailure(res, bound, "list");
3231
+ } catch (e) {
3232
+ throw classifyStoreError(e);
3233
+ }
3234
+ }
3235
+ const json = await readJsonBody(res, bound, "list");
3236
+ const files = Array.isArray(json.data) ? json.data.map((f) => makeHandle(f)) : [];
3237
+ const result = { files };
3238
+ if (typeof json.pagination_token === "string" && json.pagination_token.length > 0) {
3239
+ result.paginationToken = json.pagination_token;
3240
+ }
3241
+ return result;
3242
+ });
1776
3243
  }
1777
3244
  /**
1778
3245
  * Delete a file. Idempotent: HTTP 404 → success.
@@ -1788,34 +3255,31 @@ var XaiFileStore = class {
1788
3255
  if (typeof fileId !== "string" || fileId.trim() === "") {
1789
3256
  throw badRequest("fileId must be a non-empty string.");
1790
3257
  }
3258
+ const url = this.filesUrl(pathSegment(fileId));
1791
3259
  const failClosed = opts?.failClosed === true;
1792
- const signal = opts?.signal;
1793
- try {
1794
- const res = await this.fetchImpl(
1795
- this.filesUrl(fileId),
1796
- this.requestInit("DELETE", signal !== void 0 ? { signal } : {})
1797
- );
1798
- if (res.status === 404) {
1799
- return;
1800
- }
1801
- if (!res.ok) {
1802
- await throwHttpFailure(res);
1803
- }
1804
- } catch (err) {
1805
- if (isNotFoundError(err)) {
1806
- return;
1807
- }
1808
- const classified = signal?.aborted === true && !(err instanceof core.LlmError) ? new core.LlmError("xAI file delete aborted", {
1809
- kind: "aborted",
1810
- retryable: false,
1811
- provider: "xai",
1812
- cause: err
1813
- }) : classifyStoreError(err);
1814
- if (failClosed) {
1815
- throw classified;
3260
+ await this.bounded(opts?.signal, async (bound) => {
3261
+ try {
3262
+ const res = await this.fetchImpl(
3263
+ url,
3264
+ this.requestInit("DELETE", { signal: bound })
3265
+ );
3266
+ if (res.status === 404) {
3267
+ return;
3268
+ }
3269
+ if (!res.ok) {
3270
+ await throwHttpFailure(res, bound, "delete");
3271
+ }
3272
+ } catch (err) {
3273
+ if (isNotFoundError(err)) {
3274
+ return;
3275
+ }
3276
+ const classified = bound.aborted && !(err instanceof core.LlmError) ? abortedError(bound, "xAI file delete aborted", err) : classifyStoreError(err);
3277
+ if (failClosed) {
3278
+ throw classified;
3279
+ }
3280
+ this.onDeleteError(fileId, classified);
1816
3281
  }
1817
- this.onDeleteError(fileId, classified);
1818
- }
3282
+ });
1819
3283
  }
1820
3284
  /**
1821
3285
  * Delete many files.
@@ -1837,36 +3301,48 @@ var XaiFileStore = class {
1837
3301
  if (typeof fileId !== "string" || fileId.trim() === "") {
1838
3302
  throw badRequest("fileId must be a non-empty string.");
1839
3303
  }
1840
- let res;
1841
- try {
1842
- res = await this.fetchImpl(
1843
- this.filesUrl(`${fileId}/content`),
1844
- this.requestInit("GET", signal !== void 0 ? { signal } : {})
1845
- );
1846
- } catch (e) {
1847
- if (signal?.aborted === true) {
1848
- throw new core.LlmError("xAI file content download aborted", {
1849
- kind: "aborted",
1850
- retryable: false,
1851
- provider: "xai"
1852
- });
3304
+ const url = this.filesUrl(`${pathSegment(fileId)}/content`);
3305
+ return this.bounded(signal, async (bound) => {
3306
+ let res;
3307
+ try {
3308
+ res = await this.fetchImpl(url, this.requestInit("GET", { signal: bound }));
3309
+ } catch (e) {
3310
+ if (bound.aborted) throw abortedError(bound, "xAI file content download aborted");
3311
+ throw classifyStoreError(e);
3312
+ }
3313
+ if (res.status === 404) {
3314
+ throw notFoundError(fileId, "getContent");
3315
+ }
3316
+ if (!res.ok) {
3317
+ try {
3318
+ await throwHttpFailure(res, bound, "getContent");
3319
+ } catch (e) {
3320
+ throw classifyStoreError(e);
3321
+ }
1853
3322
  }
1854
- throw classifyStoreError(e);
1855
- }
1856
- if (res.status === 404) {
1857
- throw notFoundError(fileId, "getContent");
1858
- }
1859
- if (!res.ok) {
1860
3323
  try {
1861
- await throwHttpFailure(res);
3324
+ return new Uint8Array(await res.arrayBuffer());
1862
3325
  } catch (e) {
3326
+ if (bound.aborted)
3327
+ throw abortedError(bound, "xAI file content download aborted", e);
1863
3328
  throw classifyStoreError(e);
1864
3329
  }
1865
- }
1866
- const buf = await res.arrayBuffer();
1867
- return new Uint8Array(buf);
3330
+ });
1868
3331
  }
1869
3332
  };
3333
+ async function readJsonBody(res, signal, operation) {
3334
+ try {
3335
+ return await res.json();
3336
+ } catch (e) {
3337
+ if (signal.aborted) throw abortedError(signal, `xAI file ${operation} aborted`, e);
3338
+ throw new core.LlmError(`xAI file ${operation} returned a non-JSON body`, {
3339
+ kind: "server",
3340
+ retryable: false,
3341
+ provider: "xai",
3342
+ cause: e
3343
+ });
3344
+ }
3345
+ }
1870
3346
 
1871
3347
  // src/provider.ts
1872
3348
  function xaiProvider(opts) {
@@ -1880,11 +3356,15 @@ function xaiProvider(opts) {
1880
3356
  exports.Grok45ConfigSchema = Grok45ConfigSchema;
1881
3357
  exports.Grok46ConfigSchema = Grok46ConfigSchema;
1882
3358
  exports.Grok47ConfigSchema = Grok47ConfigSchema;
3359
+ exports.XAI_COUNT_TOKENS_TIMEOUT_MS = XAI_COUNT_TOKENS_TIMEOUT_MS;
3360
+ exports.XAI_DEFAULT_TIMEOUT_MS = XAI_DEFAULT_TIMEOUT_MS;
1883
3361
  exports.XAI_FILES_DEFAULT_BASE_URL = XAI_FILES_DEFAULT_BASE_URL;
3362
+ exports.XAI_FILES_DEFAULT_TIMEOUT_MS = XAI_FILES_DEFAULT_TIMEOUT_MS;
1884
3363
  exports.XAI_FILE_MAX_BYTES = XAI_FILE_MAX_BYTES;
1885
3364
  exports.XAI_FILE_TTL_MAX_SECONDS = XAI_FILE_TTL_MAX_SECONDS;
1886
3365
  exports.XAI_FILE_TTL_MIN_SECONDS = XAI_FILE_TTL_MIN_SECONDS;
1887
3366
  exports.XAI_PRICING = XAI_PRICING;
3367
+ exports.XAI_TIMEOUT_BUFFER_MS = XAI_TIMEOUT_BUFFER_MS;
1888
3368
  exports.XAI_TOOL_RATE_MICRO_USD = XAI_TOOL_RATE_MICRO_USD;
1889
3369
  exports.XaiFileStore = XaiFileStore;
1890
3370
  exports.buildXaiClient = buildXaiClient;