@gullabs/xai 0.8.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",
@@ -117,7 +913,31 @@ var XaiProviderOptionsSchema = zod.z.strictObject({
117
913
  parallelToolCalls: zod.z.boolean().optional().meta({
118
914
  title: "Parallel Tool Calls",
119
915
  description: "xAI Responses parallel_tool_calls. Not a generic contract field."
120
- })
916
+ }),
917
+ toolChoice: zod.z.enum(["auto", "required", "none"]).optional().meta({
918
+ title: "Server Tool Choice",
919
+ description: "xAI Responses `tool_choice` for the server-side search tools; `required` forces at least one search, `none` disables them. Requires `tools`. Cannot be combined with function tools, file attachments or the request-level toolChoice."
920
+ }),
921
+ maxTurns: zod.z.number().int().min(1).optional().meta({
922
+ title: "Max Turns",
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."
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");
121
941
  }).meta({
122
942
  title: "xAI Provider Options",
123
943
  description: "Allowlisted xAI provider options."
@@ -125,17 +945,17 @@ var XaiProviderOptionsSchema = zod.z.strictObject({
125
945
 
126
946
  // src/model-config/grok-4-5.ts
127
947
  var Grok45ConfigSchema = zod.z.strictObject({
128
- temperature: zod.z.number().optional().meta({
948
+ temperature: zod.z.number().min(0).max(2).optional().meta({
129
949
  title: "Temperature",
130
- 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.'
131
951
  }),
132
- topP: zod.z.number().optional().meta({
952
+ topP: zod.z.number().min(0).max(1).optional().meta({
133
953
  title: "Top P",
134
- 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."
135
955
  }),
136
- maxOutputTokens: zod.z.number().int().positive().optional().meta({
956
+ maxOutputTokens: core.maxOutputTokensSchema(XAI_MODEL_LIMITS["grok-4.5"]).optional().meta({
137
957
  title: "Max Output Tokens",
138
- 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."
139
959
  }),
140
960
  reasoning: zod.z.strictObject({
141
961
  effort: zod.z.enum(["low", "medium", "high"]).meta({
@@ -150,9 +970,9 @@ var Grok45ConfigSchema = zod.z.strictObject({
150
970
  title: "Service Tier",
151
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."
152
972
  }),
153
- timeoutMs: zod.z.number().int().positive().optional().meta({
973
+ timeoutMs: zod.z.number().int().positive().max(XAI_MAX_TIMEOUT_MS).optional().meta({
154
974
  title: "Timeout",
155
- description: "Logical request timeout in milliseconds."
975
+ description: "Logical request timeout in milliseconds (at most 2147478647)."
156
976
  }),
157
977
  providerOptions: zod.z.strictObject({
158
978
  xai: XaiProviderOptionsSchema.optional()
@@ -166,17 +986,17 @@ var Grok45ConfigSchema = zod.z.strictObject({
166
986
  examples: [{ reasoning: { effort: "high" } }]
167
987
  });
168
988
  var Grok46ConfigSchema = zod.z.strictObject({
169
- temperature: zod.z.number().optional().meta({
989
+ temperature: zod.z.number().min(0).max(2).optional().meta({
170
990
  title: "Temperature",
171
- 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.'
172
992
  }),
173
- topP: zod.z.number().optional().meta({
993
+ topP: zod.z.number().min(0).max(1).optional().meta({
174
994
  title: "Top P",
175
- 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."
176
996
  }),
177
- maxOutputTokens: zod.z.number().int().positive().optional().meta({
997
+ maxOutputTokens: core.maxOutputTokensSchema(XAI_MODEL_LIMITS["grok-4.6"]).optional().meta({
178
998
  title: "Max Output Tokens",
179
- 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."
180
1000
  }),
181
1001
  reasoning: zod.z.strictObject({
182
1002
  effort: zod.z.enum(["low", "medium", "high", "xhigh"]).meta({
@@ -191,9 +1011,9 @@ var Grok46ConfigSchema = zod.z.strictObject({
191
1011
  title: "Service Tier",
192
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.'
193
1013
  }),
194
- timeoutMs: zod.z.number().int().positive().optional().meta({
1014
+ timeoutMs: zod.z.number().int().positive().max(XAI_MAX_TIMEOUT_MS).optional().meta({
195
1015
  title: "Timeout",
196
- description: "Logical request timeout in milliseconds."
1016
+ description: "Logical request timeout in milliseconds (at most 2147478647)."
197
1017
  }),
198
1018
  providerOptions: zod.z.strictObject({
199
1019
  xai: XaiProviderOptionsSchema.optional()
@@ -207,17 +1027,17 @@ var Grok46ConfigSchema = zod.z.strictObject({
207
1027
  examples: [{ reasoning: { effort: "high" } }]
208
1028
  });
209
1029
  var Grok47ConfigSchema = zod.z.strictObject({
210
- temperature: zod.z.number().optional().meta({
1030
+ temperature: zod.z.number().min(0).max(2).optional().meta({
211
1031
  title: "Temperature",
212
- 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.'
213
1033
  }),
214
- topP: zod.z.number().optional().meta({
1034
+ topP: zod.z.number().min(0).max(1).optional().meta({
215
1035
  title: "Top P",
216
- 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."
217
1037
  }),
218
- maxOutputTokens: zod.z.number().int().positive().optional().meta({
1038
+ maxOutputTokens: core.maxOutputTokensSchema(XAI_MODEL_LIMITS["grok-4.7"]).optional().meta({
219
1039
  title: "Max Output Tokens",
220
- 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."
221
1041
  }),
222
1042
  reasoning: zod.z.strictObject({
223
1043
  effort: zod.z.enum(["low", "medium", "high", "xhigh"]).meta({
@@ -232,9 +1052,9 @@ var Grok47ConfigSchema = zod.z.strictObject({
232
1052
  title: "Service Tier",
233
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.'
234
1054
  }),
235
- timeoutMs: zod.z.number().int().positive().optional().meta({
1055
+ timeoutMs: zod.z.number().int().positive().max(XAI_MAX_TIMEOUT_MS).optional().meta({
236
1056
  title: "Timeout",
237
- description: "Logical request timeout in milliseconds."
1057
+ description: "Logical request timeout in milliseconds (at most 2147478647)."
238
1058
  }),
239
1059
  providerOptions: zod.z.strictObject({
240
1060
  xai: XaiProviderOptionsSchema.optional()
@@ -252,37 +1072,39 @@ var Grok47ConfigSchema = zod.z.strictObject({
252
1072
  var grok45ModelDescriptor = {
253
1073
  model: "grok-4.5",
254
1074
  provider: "xai",
1075
+ limits: XAI_MODEL_LIMITS["grok-4.5"],
255
1076
  pricingFamily: "grok-4.5",
256
1077
  capabilities: {
1078
+ inputMimeTypes: XAI_INPUT_MIME_TYPES,
257
1079
  reasoning: true,
258
1080
  reasoningApi: "level",
259
1081
  admittedReasoningEfforts: ["low", "medium", "high"],
260
1082
  structuredOutput: true,
261
1083
  nativeStructuredOutput: true,
262
- vision: true,
263
- audioInput: false,
264
1084
  sampling: "tunable",
265
1085
  caching: { explicit: false, minTokens: 0 },
266
1086
  grounding: true,
1087
+ structuredOutputWithTools: true,
267
1088
  functionCalling: true,
268
1089
  serviceTiers: ["priority"]
269
1090
  },
270
1091
  configSchema: Grok45ConfigSchema,
1092
+ configKeys: core.toConfigKeys(Grok45ConfigSchema),
271
1093
  configJsonSchema: core.toConfigJsonSchema(Grok45ConfigSchema),
272
1094
  validateConfig: core.zodToStandardSchema(Grok45ConfigSchema)
273
1095
  };
274
1096
  var grok46ModelDescriptor = {
275
1097
  model: "grok-4.6",
276
1098
  provider: "xai",
1099
+ limits: XAI_MODEL_LIMITS["grok-4.6"],
277
1100
  pricingFamily: "grok-4.6",
278
1101
  capabilities: {
1102
+ inputMimeTypes: XAI_INPUT_MIME_TYPES,
279
1103
  reasoning: true,
280
1104
  reasoningApi: "level",
281
1105
  admittedReasoningEfforts: ["low", "medium", "high", "xhigh"],
282
1106
  structuredOutput: true,
283
1107
  nativeStructuredOutput: true,
284
- vision: true,
285
- audioInput: false,
286
1108
  sampling: "tunable",
287
1109
  caching: { explicit: false, minTokens: 0 },
288
1110
  grounding: true,
@@ -291,29 +1113,33 @@ var grok46ModelDescriptor = {
291
1113
  serviceTiers: ["priority"]
292
1114
  },
293
1115
  configSchema: Grok46ConfigSchema,
1116
+ configKeys: core.toConfigKeys(Grok46ConfigSchema),
294
1117
  configJsonSchema: core.toConfigJsonSchema(Grok46ConfigSchema),
295
1118
  validateConfig: core.zodToStandardSchema(Grok46ConfigSchema)
296
1119
  };
297
1120
  var grok47ModelDescriptor = {
298
1121
  model: "grok-4.7",
299
1122
  provider: "xai",
1123
+ limits: XAI_MODEL_LIMITS["grok-4.7"],
300
1124
  pricingFamily: "grok-4.7",
301
1125
  capabilities: {
1126
+ inputMimeTypes: XAI_INPUT_MIME_TYPES,
302
1127
  reasoning: true,
303
1128
  reasoningApi: "level",
304
1129
  admittedReasoningEfforts: ["low", "medium", "high", "xhigh"],
305
1130
  structuredOutput: true,
306
1131
  nativeStructuredOutput: true,
307
- vision: true,
308
- audioInput: false,
309
1132
  sampling: "tunable",
310
1133
  caching: { explicit: false, minTokens: 0 },
311
1134
  grounding: true,
1135
+ structuredOutputWithTools: true,
312
1136
  functionCalling: true,
313
- statelessReasoningReplay: true,
1137
+ continuation: "state",
1138
+ providerState: true,
314
1139
  serviceTiers: ["priority"]
315
1140
  },
316
1141
  configSchema: Grok47ConfigSchema,
1142
+ configKeys: core.toConfigKeys(Grok47ConfigSchema),
317
1143
  configJsonSchema: core.toConfigJsonSchema(Grok47ConfigSchema),
318
1144
  validateConfig: core.zodToStandardSchema(Grok47ConfigSchema)
319
1145
  };
@@ -323,6 +1149,48 @@ var xaiModelDescriptors = [
323
1149
  grok47ModelDescriptor
324
1150
  ];
325
1151
  var xaiRegistry = core.createModelRegistry(xaiModelDescriptors);
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
+ };
326
1194
  var xaiPricingVersion = "xai-2026-09-25";
327
1195
  var XAI_TOOL_RATE_MICRO_USD = {
328
1196
  web_search_calls: 5e3,
@@ -336,6 +1204,51 @@ var XAI_TOOL_COUNTER_KEYS = [
336
1204
  ];
337
1205
  var X_SEARCH_ITEM_COUNTERS = ["x_posts_fetched", "x_users_fetched"];
338
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
+ }
339
1252
  var XAI_PRICING = Object.freeze({
340
1253
  // ── grok-4.5 ── $2.00/$6.00 (<200k), $4.00/$12.00 (>=200k); cached $0.30/$0.60
341
1254
  "grok-4.5": {
@@ -413,6 +1326,11 @@ function scaleRates(rates, factor) {
413
1326
  return scaled;
414
1327
  }
415
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) {
416
1334
  const listed = lookupConcreteRates(model, tier);
417
1335
  if (listed === void 0) {
418
1336
  return core.computeCost(model, usage, tier, lookupConcreteRates, xaiPricingVersion);
@@ -448,11 +1366,12 @@ function computeXaiCost(model, usage, tier) {
448
1366
  return sum + Math.round(count * XAI_TOOL_RATE_MICRO_USD[key]);
449
1367
  }, 0);
450
1368
  const microUsd = inputCost + cachedCost + outputCost + toolsCost;
1369
+ const unpricedCounters = unpricedXaiToolCounters(usage);
451
1370
  return {
452
1371
  microUsd,
453
1372
  usd: microUsd / 1e6,
454
1373
  pricingVersion: xaiPricingVersion,
455
- confidence: missingWebCounter || attachmentUnpinned ? "estimated" : "exact",
1374
+ confidence: missingWebCounter || attachmentUnpinned || unpricedCounters.length > 0 || usage.details[XAI_ESTIMATED_USAGE_KEY] === 1 ? "estimated" : "exact",
456
1375
  details: {
457
1376
  input: inputCost,
458
1377
  cached: cachedCost,
@@ -486,22 +1405,42 @@ function isXaiMessageItem(item) {
486
1405
  function isXaiReasoningItem(item) {
487
1406
  return item.type === "reasoning" && Array.isArray(item.summary);
488
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
+ }
489
1414
  function badXaiRequest(message) {
490
1415
  return new core.LlmError(message, { kind: "bad_request", retryable: false });
491
1416
  }
492
- var ALLOWED_XAI_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/jpg", "image/png"]);
493
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
+ }
494
1438
  function mapPart(p) {
495
1439
  switch (p.kind) {
496
1440
  case "text":
497
1441
  return { type: "input_text", text: p.text };
498
1442
  case "inline-media": {
499
- if (!ALLOWED_XAI_IMAGE_MIME_TYPES.has(p.mimeType)) {
500
- throw badXaiRequest(
501
- `xAI vision only supports image/jpeg and image/png; got mimeType "${p.mimeType}".`
502
- );
503
- }
504
- const byteLength = Buffer.from(p.data, "base64").length;
1443
+ const byteLength = decodedBase64Length(p.data);
505
1444
  if (byteLength > MAX_XAI_INLINE_IMAGE_BYTES) {
506
1445
  throw badXaiRequest(
507
1446
  `xAI inline images must be at most 20 MiB; got ${byteLength} bytes.`
@@ -511,8 +1450,7 @@ function mapPart(p) {
511
1450
  }
512
1451
  case "file-uri": {
513
1452
  const isPublicHttpUrl = p.uri.startsWith("http://") || p.uri.startsWith("https://");
514
- const isAllowedImageType = ALLOWED_XAI_IMAGE_MIME_TYPES.has(p.mimeType);
515
- if (!isPublicHttpUrl || !isAllowedImageType) {
1453
+ if (!isPublicHttpUrl) {
516
1454
  throw badXaiRequest(
517
1455
  `xAI only accepts public http(s) image URLs via FileUriPart; got scheme of "${p.uri}" / mimeType "${p.mimeType}".`
518
1456
  );
@@ -539,7 +1477,80 @@ function mapPart(p) {
539
1477
  return core.assertNever(p);
540
1478
  }
541
1479
  }
542
- var XAI_PROVIDER_OPTION_KEYS = /* @__PURE__ */ new Set(["promptCacheKey", "tools", "parallelToolCalls"]);
1480
+ var XAI_PROVIDER_OPTION_KEYS = /* @__PURE__ */ new Set([
1481
+ "promptCacheKey",
1482
+ "tools",
1483
+ "parallelToolCalls",
1484
+ "toolChoice",
1485
+ "maxTurns",
1486
+ "searchBudget"
1487
+ ]);
1488
+ var XAI_DEFAULT_SCHEMA_NAME = "structured_output";
1489
+ var XAI_SCHEMA_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
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
+ }
543
1554
  function mapXaiProviderOptions(xaiOpts, model) {
544
1555
  if (xaiOpts === void 0) {
545
1556
  return {};
@@ -554,7 +1565,7 @@ function mapXaiProviderOptions(xaiOpts, model) {
554
1565
  throw badXaiRequest(
555
1566
  `providerOptions.xai contains unsupported keys [${unknownKeys.join(
556
1567
  ", "
557
- )}] for model "${model}". Allowed keys: promptCacheKey, tools, parallelToolCalls.`
1568
+ )}] for model "${model}". Allowed keys: promptCacheKey, tools, parallelToolCalls, toolChoice, maxTurns, searchBudget.`
558
1569
  );
559
1570
  }
560
1571
  const mapped = {};
@@ -577,15 +1588,53 @@ function mapXaiProviderOptions(xaiOpts, model) {
577
1588
  }
578
1589
  mapped.parallelToolCalls = xaiOpts["parallelToolCalls"];
579
1590
  }
1591
+ const toolChoice = xaiOpts["toolChoice"];
1592
+ if (toolChoice !== void 0) {
1593
+ if (typeof toolChoice !== "string" || !XAI_SERVER_TOOL_CHOICES.has(toolChoice)) {
1594
+ throw badXaiRequest(
1595
+ `providerOptions.xai.toolChoice must be "auto", "required" or "none" for model "${model}".`
1596
+ );
1597
+ }
1598
+ if (mapped.tools === void 0 || mapped.tools.length === 0) {
1599
+ throw badXaiRequest(
1600
+ `providerOptions.xai.toolChoice requires a non-empty providerOptions.xai.tools for model "${model}".`
1601
+ );
1602
+ }
1603
+ mapped.toolChoice = toolChoice;
1604
+ }
1605
+ const maxTurns = xaiOpts["maxTurns"];
1606
+ if (maxTurns !== void 0) {
1607
+ if (typeof maxTurns !== "number" || !Number.isInteger(maxTurns) || maxTurns < 1) {
1608
+ throw badXaiRequest(
1609
+ `providerOptions.xai.maxTurns must be an integer >= 1 for model "${model}".`
1610
+ );
1611
+ }
1612
+ if (mapped.tools === void 0 || mapped.tools.length === 0) {
1613
+ throw badXaiRequest(
1614
+ `providerOptions.xai.maxTurns requires a non-empty providerOptions.xai.tools for model "${model}".`
1615
+ );
1616
+ }
1617
+ mapped.maxTurns = maxTurns;
1618
+ }
1619
+ if (xaiOpts["searchBudget"] !== void 0) {
1620
+ mapped.searchBudget = mapXaiSearchBudget(xaiOpts["searchBudget"], mapped.tools, model);
1621
+ }
580
1622
  return mapped;
581
1623
  }
582
1624
  function parseXaiReplayState(value, model) {
583
1625
  if (value === void 0) return void 0;
584
- 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(
585
1634
  (item) => !isPlainRecord(item) || typeof item["type"] !== "string" && typeof item["role"] !== "string"
586
1635
  )) {
587
1636
  throw badXaiRequest(
588
- `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}".`
589
1638
  );
590
1639
  }
591
1640
  return value;
@@ -719,35 +1768,56 @@ function isXaiSafetyCheckBody(rawErr) {
719
1768
  const text = extractXaiErrorBodyText(rawErr);
720
1769
  return text !== void 0 && text.startsWith(XAI_SAFETY_CHECK_MESSAGE_PREFIX);
721
1770
  }
722
- var XAI_TRANSPORT_ERROR_PATTERN = /connection error|econnreset|econnrefused|etimedout|eai_again|epipe|socket hang up|fetch failed/i;
723
- function matchesXaiTransportSignature(err) {
724
- if (!(err instanceof Error)) return false;
725
- if (XAI_TRANSPORT_ERROR_PATTERN.test(err.message)) return true;
726
- const code = err.code;
727
- return typeof code === "string" && XAI_TRANSPORT_ERROR_PATTERN.test(code);
728
- }
729
- function isXaiTransportError(rawErr) {
730
- if (!(rawErr instanceof Error)) return false;
731
- const ctorName = rawErr.constructor.name;
732
- if (ctorName === "APIConnectionError" || ctorName === "APIConnectionTimeoutError") {
733
- return true;
734
- }
735
- if (matchesXaiTransportSignature(rawErr)) return true;
736
- const cause = rawErr.cause;
737
- if (cause instanceof Error) {
738
- const causeCtorName = cause.constructor.name;
739
- if (causeCtorName === "APIConnectionError" || causeCtorName === "APIConnectionTimeoutError") {
740
- return true;
741
- }
742
- if (matchesXaiTransportSignature(cause)) return true;
743
- }
744
- 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");
745
1778
  }
746
- 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) {
747
1803
  if (rawErr instanceof core.LlmError) {
748
1804
  return rawErr;
749
1805
  }
1806
+ if (rawErr instanceof XaiStreamError) {
1807
+ return classifyStreamError(rawErr, estimatedInputTokens, requestTimeoutMs);
1808
+ }
750
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
+ }
751
1821
  if (base.httpStatus === 400 && isXaiAuthFailureBody(rawErr)) {
752
1822
  return new core.LlmError(base.message, {
753
1823
  kind: "invalid_auth",
@@ -767,7 +1837,23 @@ function classifyXaiError(rawErr) {
767
1837
  cause: base.cause ?? rawErr
768
1838
  });
769
1839
  }
770
- if (base.kind === "unknown" && isXaiTransportError(rawErr)) {
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)) {
771
1857
  return new core.LlmError(base.message, {
772
1858
  kind: "server",
773
1859
  retryable: true,
@@ -775,16 +1861,233 @@ function classifyXaiError(rawErr) {
775
1861
  cause: base.cause ?? rawErr
776
1862
  });
777
1863
  }
778
- return new core.LlmError(base.message, {
779
- kind: base.kind,
780
- retryable: base.retryable,
781
- ...base.httpStatus !== void 0 ? { httpStatus: base.httpStatus } : {},
782
- ...base.retryAfterMs !== void 0 ? { retryAfterMs: base.retryAfterMs } : {},
783
- provider: "xai",
784
- cause: base.cause ?? rawErr
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,
2004
+ provider: "xai",
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;
2027
+ }
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"
785
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
+ };
786
2072
  }
787
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
+ }
788
2091
  return {
789
2092
  id: "xai",
790
2093
  async run(req, ctx) {
@@ -796,12 +2099,16 @@ function xaiAdapter(opts) {
796
2099
  }
797
2100
  const warnings = [];
798
2101
  const model = req.model;
799
- if (req.modelDescriptor !== void 0 && (req.modelDescriptor.model !== model || req.modelDescriptor.provider !== "xai")) {
800
- 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");
801
2108
  }
802
- 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") {
803
2110
  throw badXaiRequest(
804
- `A matching xAI model descriptor with statelessReasoningReplay is required for "${model}".`
2111
+ `A matching xAI model descriptor with continuation "state" is required for "${model}".`
805
2112
  );
806
2113
  }
807
2114
  const genConfig = req.config;
@@ -809,11 +2116,11 @@ function xaiAdapter(opts) {
809
2116
  genConfig.providerOptions?.["xai"],
810
2117
  model
811
2118
  );
812
- const replayRequired = req.modelDescriptor?.capabilities?.statelessReasoningReplay === true;
2119
+ const replayRequired = req.modelDescriptor?.capabilities?.continuation === "state";
813
2120
  const replayState = parseXaiReplayState(req.transientProviderState, model);
814
2121
  if (replayState !== void 0 && !replayRequired) {
815
2122
  throw badXaiRequest(
816
- `transientProviderState requires a statelessReasoningReplay model descriptor for "${model}".`
2123
+ `transientProviderState requires a model descriptor with continuation "state" for "${model}".`
817
2124
  );
818
2125
  }
819
2126
  if (replayState !== void 0 && req.messages.length === 0) {
@@ -821,12 +2128,12 @@ function xaiAdapter(opts) {
821
2128
  `Stateless conversation replay for model "${model}" requires new messages to append.`
822
2129
  );
823
2130
  }
824
- const input = [...replayState?.input ?? []];
2131
+ const input = [...replayState?.xai.input ?? []];
825
2132
  const replayCallIds = new Set(
826
- 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") ?? []
827
2134
  );
828
2135
  const replayedResultIds = new Set(
829
- replayState?.input.filter(
2136
+ replayState?.xai.input.filter(
830
2137
  (item) => isPlainRecord(item) && item["type"] === "function_call_output"
831
2138
  ).map((item) => isPlainRecord(item) ? item["call_id"] : void 0).filter((id) => typeof id === "string") ?? []
832
2139
  );
@@ -946,7 +2253,18 @@ function xaiAdapter(opts) {
946
2253
  const structuredOutputRequested = req.outputJsonSchema !== void 0;
947
2254
  if (structuredOutputRequested) {
948
2255
  const schema = req.outputJsonSchema;
949
- 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;
950
2268
  params.text = {
951
2269
  format: { type: "json_schema", name, schema, strict: true }
952
2270
  };
@@ -972,6 +2290,27 @@ function xaiAdapter(opts) {
972
2290
  );
973
2291
  }
974
2292
  params.tools = searchTools;
2293
+ if (xaiProviderConfig.toolChoice !== void 0) {
2294
+ if (req.toolChoice !== void 0) {
2295
+ throw badXaiRequest(
2296
+ `providerOptions.xai.toolChoice and toolChoice cannot both be set for model "${model}"; xAI accepts one tool_choice per request.`
2297
+ );
2298
+ }
2299
+ if (req.tools !== void 0 && req.tools.length > 0) {
2300
+ throw badXaiRequest(
2301
+ `providerOptions.xai.toolChoice applies to the server-side search tools only and cannot be combined with function tools for model "${model}".`
2302
+ );
2303
+ }
2304
+ if (hasFileRef) {
2305
+ throw badXaiRequest(
2306
+ `providerOptions.xai.toolChoice cannot be combined with file attachments for model "${model}"; xAI's implicit attachment_search would count as the tool call.`
2307
+ );
2308
+ }
2309
+ params.tool_choice = xaiProviderConfig.toolChoice;
2310
+ }
2311
+ if (xaiProviderConfig.maxTurns !== void 0) {
2312
+ params.max_turns = xaiProviderConfig.maxTurns;
2313
+ }
975
2314
  }
976
2315
  if (req.tools !== void 0 && req.tools.length > 0) {
977
2316
  if (req.modelDescriptor?.capabilities?.functionCalling !== true) {
@@ -979,6 +2318,13 @@ function xaiAdapter(opts) {
979
2318
  `tools is not supported for xai model "${model}" (capabilities.functionCalling is not true).`
980
2319
  );
981
2320
  }
2321
+ req.tools.forEach((tool, index) => {
2322
+ core.assertJsonSchemaProfile(
2323
+ tool.inputJsonSchema,
2324
+ `tools[${index}].inputJsonSchema`,
2325
+ XAI_JSON_SCHEMA_PROFILE
2326
+ );
2327
+ });
982
2328
  const functionTools = req.tools.map((tool) => ({
983
2329
  type: "function",
984
2330
  name: tool.name,
@@ -991,129 +2337,300 @@ function xaiAdapter(opts) {
991
2337
  }
992
2338
  }
993
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
+ }
994
2345
  params.parallel_tool_calls = xaiProviderConfig.parallelToolCalls;
995
2346
  }
996
2347
  let response;
2348
+ let responseMeta;
2349
+ let sdkCallStart;
997
2350
  try {
998
- const buildClient = opts?._clientFactory ?? buildXaiClient;
999
- 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);
1000
2353
  ctx.logger.debug(
1001
2354
  { model, configKeys: Object.keys(params) },
1002
2355
  "llm.adapter.dispatch"
1003
2356
  );
1004
- response = await client.responses.create(
1005
- params,
1006
- ctx.signal !== void 0 ? { signal: ctx.signal } : void 0
1007
- );
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);
1008
2367
  } catch (rawErr) {
1009
- 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
+ );
1010
2391
  }
1011
- let text = "";
1012
- let reasoningText;
1013
- const messageItems = [];
1014
- const toolCalls = [];
1015
- for (const item of response.output) {
1016
- if (isXaiMessageItem(item)) {
1017
- messageItems.push(item);
1018
- } else if (isXaiReasoningItem(item)) {
1019
- const joined = item.summary.map((s) => s.text).join("");
1020
- if (joined.length > 0) {
1021
- reasoningText = (reasoningText ?? "") + joined;
2392
+ for (const message of responseMeta?.streamNotes ?? []) {
2393
+ warnings.push({ type: "other", message });
2394
+ }
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
+ }
1022
2445
  }
1023
- } else if (item.type === "function_call") {
1024
- const callId = typeof item["call_id"] === "string" ? item["call_id"] : "";
1025
- const name = typeof item["name"] === "string" ? item["name"] : "";
1026
- let args = {};
1027
- if (typeof item["arguments"] === "string") {
1028
- try {
1029
- args = JSON.parse(item["arguments"]);
1030
- } catch {
1031
- 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);
1032
2462
  }
1033
2463
  }
1034
- if (callId.length > 0 && name.length > 0) {
1035
- 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
+ });
1036
2475
  }
1037
2476
  }
1038
- }
1039
- if (messageItems.length > 0) {
1040
- const lastMessage = messageItems[messageItems.length - 1];
1041
- text = lastMessage.content.map((part) => part.text).join("");
1042
- 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";
1043
2497
  warnings.push({
1044
2498
  type: "other",
1045
- 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}".`
1046
2500
  });
1047
2501
  }
1048
- }
1049
- let rawStructured;
1050
- if (structuredOutputRequested && text.length > 0) {
1051
- try {
1052
- rawStructured = JSON.parse(text);
1053
- } 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
+ });
1054
2509
  }
1055
- }
1056
- const usage = mapUsage(response.usage);
1057
- const finishReason = mapFinishReason(response);
1058
- const expectedToolCounters = expectedServerToolCounters(
1059
- xaiProviderConfig.tools);
1060
- if (expectedToolCounters.length > 0 || hasFileRef) {
1061
- usage.details["server_tools_requested"] = 1;
1062
- if (xaiProviderConfig.tools?.some((tool) => tool["type"] === "x_search") === true) {
1063
- 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
+ }
1064
2525
  }
1065
- const missing = expectedToolCounters.filter((key) => !(key in usage.details));
1066
- if (missing.length > 0) {
1067
- 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) {
1068
2530
  warnings.push({
1069
2531
  type: "other",
1070
- message: `xai: server tools were requested but usage is missing counters [${missing.join(
1071
- ", "
1072
- )}]; 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.`
1073
2533
  });
1074
2534
  }
1075
- if (hasFileRef) {
1076
- 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;
1077
2539
  warnings.push({
1078
2540
  type: "other",
1079
- 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.`
1080
2542
  });
1081
2543
  }
1082
- }
1083
- const citations = collectXaiCitations(response, messageItems);
1084
- const providerMeta = {};
1085
- const contextDetails = response.usage["context_details"];
1086
- if (isPlainRecord(contextDetails)) {
1087
- providerMeta["context_details"] = contextDetails;
1088
- }
1089
- if (isPlainRecord(response.metadata)) {
1090
- providerMeta["metadata"] = response.metadata;
1091
- }
1092
- let transientProviderState;
1093
- if (replayRequired) {
1094
- const state = {
1095
- model,
1096
- 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" } : {}
1097
2618
  };
1098
- transientProviderState = state;
1099
- }
1100
- const servedServiceTier = typeof response.service_tier === "string" && response.service_tier.length > 0 ? response.service_tier : void 0;
1101
- const result = {
1102
- model: response.model,
1103
- usage,
1104
- warnings,
1105
- finishReason,
1106
- responseId: response.id,
1107
- ...text.length > 0 ? { text } : {},
1108
- ...reasoningText !== void 0 ? { reasoningText } : {},
1109
- ...rawStructured !== void 0 ? { rawStructured } : {},
1110
- ...servedServiceTier !== void 0 ? { servedServiceTier } : {},
1111
- ...Object.keys(providerMeta).length > 0 ? { providerMetadata: providerMeta } : {},
1112
- ...transientProviderState !== void 0 ? { transientProviderState } : {},
1113
- ...citations.length > 0 ? { citations } : {},
1114
- ...toolCalls.length > 0 ? { toolCalls, finishReason: "tool_calls" } : {}
1115
- };
1116
- 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
+ }
1117
2634
  },
1118
2635
  async countTokens(req, ctx) {
1119
2636
  if (req.provider !== "xai") {
@@ -1129,16 +2646,32 @@ function xaiAdapter(opts) {
1129
2646
  }
1130
2647
  const text = concatenateTokenizeText(req);
1131
2648
  const apiKey = requireApiKey(ctx.auth);
1132
- 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);
1133
2663
  try {
1134
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,
1135
2668
  method: "POST",
1136
2669
  headers: {
1137
2670
  Authorization: `Bearer ${apiKey}`,
1138
2671
  "Content-Type": "application/json"
1139
2672
  },
1140
2673
  body: JSON.stringify({ model: req.model, text }),
1141
- ...ctx.signal !== void 0 ? { signal: ctx.signal } : {}
2674
+ signal: controller.signal
1142
2675
  });
1143
2676
  if (!res.ok) {
1144
2677
  let parsed;
@@ -1147,12 +2680,26 @@ function xaiAdapter(opts) {
1147
2680
  } catch {
1148
2681
  parsed = await res.text().catch(() => "");
1149
2682
  }
1150
- throw Object.assign(new Error(`xAI tokenize-text HTTP ${res.status}`), {
1151
- status: res.status,
1152
- 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
1153
2701
  });
1154
2702
  }
1155
- const raw = await res.json();
1156
2703
  if (!isPlainRecord(raw) || !Array.isArray(raw["token_ids"])) {
1157
2704
  throw new core.LlmError(
1158
2705
  "xAI tokenize-text response is malformed: missing required field: token_ids",
@@ -1167,6 +2714,9 @@ function xaiAdapter(opts) {
1167
2714
  raw
1168
2715
  };
1169
2716
  } catch (rawErr) {
2717
+ if (controller.signal.aborted && controller.signal.reason === timeout) {
2718
+ throw timeout;
2719
+ }
1170
2720
  if (rawErr instanceof Error && rawErr.name === "AbortError") {
1171
2721
  throw new core.LlmError("xAI tokenize-text aborted", {
1172
2722
  kind: "aborted",
@@ -1176,12 +2726,15 @@ function xaiAdapter(opts) {
1176
2726
  });
1177
2727
  }
1178
2728
  throw classifyXaiError(rawErr);
2729
+ } finally {
2730
+ clearTimeout(timer);
2731
+ ctx.signal?.removeEventListener("abort", forwardAbort);
1179
2732
  }
1180
2733
  }
1181
2734
  };
1182
2735
  }
1183
2736
  var WEB_SEARCH_COUNTER = "web_search_calls";
1184
- function expectedServerToolCounters(tools, _hasFileRef) {
2737
+ function expectedServerToolCounters(tools) {
1185
2738
  const keys = [];
1186
2739
  if (tools !== void 0) {
1187
2740
  for (const tool of tools) {
@@ -1191,49 +2744,71 @@ function expectedServerToolCounters(tools, _hasFileRef) {
1191
2744
  }
1192
2745
  return keys;
1193
2746
  }
1194
- function collectXaiCitations(response, messageItems) {
1195
- const seen = /* @__PURE__ */ new Set();
1196
- const citations = [];
1197
- const push = (url, title) => {
1198
- if (typeof url !== "string" || url.length === 0) return;
1199
- if (seen.has(url)) return;
1200
- seen.add(url);
1201
- const citation = { url };
1202
- if (typeof title === "string" && title.length > 0 && title !== url) {
1203
- citation.title = title;
1204
- }
1205
- try {
1206
- const parsed = new URL(url);
1207
- if (parsed.hostname.length > 0) {
1208
- 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 {
1209
2764
  }
1210
- } catch {
2765
+ byUrl.set(url, citation);
1211
2766
  }
1212
- 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;
1213
2771
  };
1214
2772
  if (Array.isArray(response.citations)) {
1215
2773
  for (const item of response.citations) {
1216
2774
  if (typeof item === "string") {
1217
- push(item, void 0);
2775
+ upsert(item, void 0);
1218
2776
  } else if (isPlainRecord(item)) {
1219
- push(item["url"] ?? item["uri"], item["title"]);
2777
+ upsert(item["url"] ?? item["uri"], item["title"]);
1220
2778
  }
1221
2779
  }
1222
2780
  }
1223
2781
  const lastMessage = messageItems.at(-1);
1224
- const citationMessages = lastMessage === void 0 ? [] : [lastMessage];
1225
- for (const item of citationMessages) {
1226
- for (const part of item.content) {
1227
- const annotations = part.annotations;
1228
- if (!Array.isArray(annotations)) continue;
1229
- for (const ann of annotations) {
1230
- if (!isPlainRecord(ann)) continue;
1231
- if (ann["type"] !== void 0 && ann["type"] !== "url_citation") continue;
1232
- 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
+ }
1233
2807
  }
2808
+ partOffset += (partText(part) ?? "").length;
1234
2809
  }
1235
2810
  }
1236
- return citations;
2811
+ return [...byUrl.values()];
1237
2812
  }
1238
2813
  function concatenateTokenizeText(req) {
1239
2814
  const chunks = [];
@@ -1265,6 +2840,7 @@ var XAI_FILE_TTL_MIN_SECONDS = 3600;
1265
2840
  var XAI_FILE_TTL_MAX_SECONDS = 2592e3;
1266
2841
  var XAI_FILE_MAX_BYTES = 48 * 1024 * 1024;
1267
2842
  var XAI_FILES_DEFAULT_BASE_URL = "https://api.x.ai/v1";
2843
+ var XAI_FILES_DEFAULT_TIMEOUT_MS = 6e4;
1268
2844
  function isPlainRecord2(value) {
1269
2845
  return typeof value === "object" && value !== null && !Array.isArray(value);
1270
2846
  }
@@ -1333,19 +2909,23 @@ function toBlob(data, mimeType) {
1333
2909
  var XaiFilesHttpError = class extends Error {
1334
2910
  status;
1335
2911
  error;
1336
- constructor(status, message, errorBody) {
2912
+ /** The response headers: `classifyError` reads `Retry-After` from them. */
2913
+ headers;
2914
+ constructor(status, message, errorBody, headers) {
1337
2915
  super(message);
1338
2916
  this.name = "XaiFilesHttpError";
1339
2917
  this.status = status;
1340
2918
  this.error = errorBody;
2919
+ this.headers = headers;
1341
2920
  }
1342
2921
  };
1343
- async function throwHttpFailure(res) {
2922
+ async function throwHttpFailure(res, signal, operation) {
1344
2923
  const status = res.status;
1345
2924
  let bodyText = "";
1346
2925
  try {
1347
2926
  bodyText = await res.text();
1348
- } catch {
2927
+ } catch (e) {
2928
+ if (signal.aborted) throw abortedError(signal, `xAI file ${operation} aborted`, e);
1349
2929
  bodyText = "";
1350
2930
  }
1351
2931
  let parsed = bodyText;
@@ -1362,7 +2942,9 @@ async function throwHttpFailure(res) {
1362
2942
  } else if (isPlainRecord2(parsed) && typeof parsed["error"] === "string") {
1363
2943
  message = parsed["error"];
1364
2944
  }
1365
- 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);
1366
2948
  }
1367
2949
  function isNotFoundError(err) {
1368
2950
  if (err instanceof core.LlmError && err.httpStatus === 404) {
@@ -1431,16 +3013,41 @@ function classifyStoreError(raw) {
1431
3013
  cause: classified.cause ?? raw
1432
3014
  });
1433
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
+ }
1434
3033
  var XaiFileStore = class {
1435
3034
  apiKey;
1436
3035
  baseUrl;
1437
3036
  fetchImpl;
3037
+ timeoutMs;
1438
3038
  onDeleteError;
1439
3039
  logger;
1440
3040
  constructor(opts) {
1441
3041
  this.apiKey = requireApiKey(opts.auth);
1442
3042
  this.baseUrl = (opts.baseUrl ?? XAI_FILES_DEFAULT_BASE_URL).replace(/\/+$/, "");
1443
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;
1444
3051
  this.logger = opts.logger;
1445
3052
  this.onDeleteError = opts.onDeleteError ?? ((fileId, err) => {
1446
3053
  const message = core.redactSecrets(core.classifyError(err).message);
@@ -1474,6 +3081,37 @@ var XaiFileStore = class {
1474
3081
  }
1475
3082
  return init;
1476
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
+ }
1477
3115
  /**
1478
3116
  * Upload bytes to xAI Files. Returns immediately with metadata (no poll).
1479
3117
  *
@@ -1505,77 +3143,64 @@ var XaiFileStore = class {
1505
3143
  }
1506
3144
  form.append("purpose", purpose);
1507
3145
  form.append("file", toBlob(input.data, input.mimeType), input.filename);
1508
- let res;
1509
- try {
1510
- res = await this.fetchImpl(
1511
- this.filesUrl(),
1512
- this.requestInit("POST", {
1513
- body: form,
1514
- ...signal !== void 0 ? { signal } : {}
1515
- })
1516
- );
1517
- } catch (e) {
1518
- if (signal?.aborted === true) {
1519
- throw new core.LlmError("xAI file upload aborted", {
1520
- kind: "aborted",
1521
- retryable: false,
1522
- provider: "xai"
1523
- });
1524
- }
1525
- throw classifyStoreError(e);
1526
- }
1527
- if (!res.ok) {
3146
+ return this.bounded(signal, async (bound) => {
3147
+ let res;
1528
3148
  try {
1529
- await throwHttpFailure(res);
3149
+ res = await this.fetchImpl(
3150
+ this.filesUrl(),
3151
+ this.requestInit("POST", { body: form, signal: bound })
3152
+ );
1530
3153
  } catch (e) {
3154
+ if (bound.aborted) throw abortedError(bound, "xAI file upload aborted");
1531
3155
  throw classifyStoreError(e);
1532
3156
  }
1533
- }
1534
- let json;
1535
- try {
1536
- json = await res.json();
1537
- } catch (e) {
1538
- throw new core.LlmError("xAI file upload returned non-JSON body", {
1539
- kind: "server",
1540
- retryable: false,
1541
- provider: "xai",
1542
- cause: e
1543
- });
1544
- }
1545
- 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
+ });
1546
3178
  }
1547
3179
  async get(fileId, signal) {
1548
3180
  if (typeof fileId !== "string" || fileId.trim() === "") {
1549
3181
  throw badRequest("fileId must be a non-empty string.");
1550
3182
  }
1551
- let res;
1552
- try {
1553
- res = await this.fetchImpl(
1554
- this.filesUrl(fileId),
1555
- this.requestInit("GET", signal !== void 0 ? { signal } : {})
1556
- );
1557
- } catch (e) {
1558
- if (signal?.aborted === true) {
1559
- throw new core.LlmError("xAI file get aborted", {
1560
- kind: "aborted",
1561
- retryable: false,
1562
- provider: "xai"
1563
- });
1564
- }
1565
- throw classifyStoreError(e);
1566
- }
1567
- if (res.status === 404) {
1568
- throw notFoundError(fileId, "get");
1569
- }
1570
- if (!res.ok) {
3183
+ const url = this.filesUrl(pathSegment(fileId));
3184
+ return this.bounded(signal, async (bound) => {
3185
+ let res;
1571
3186
  try {
1572
- await throwHttpFailure(res);
3187
+ res = await this.fetchImpl(url, this.requestInit("GET", { signal: bound }));
1573
3188
  } catch (e) {
3189
+ if (bound.aborted) throw abortedError(bound, "xAI file get aborted");
1574
3190
  throw classifyStoreError(e);
1575
3191
  }
1576
- }
1577
- const json = await res.json();
1578
- 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
+ });
1579
3204
  }
1580
3205
  async list(opts = {}, signal) {
1581
3206
  if (opts.limit !== void 0) {
@@ -1592,36 +3217,29 @@ var XaiFileStore = class {
1592
3217
  }
1593
3218
  const qs = params.toString();
1594
3219
  const url = qs.length > 0 ? `${this.filesUrl()}?${qs}` : this.filesUrl();
1595
- let res;
1596
- try {
1597
- res = await this.fetchImpl(
1598
- url,
1599
- this.requestInit("GET", signal !== void 0 ? { signal } : {})
1600
- );
1601
- } catch (e) {
1602
- if (signal?.aborted === true) {
1603
- throw new core.LlmError("xAI file list aborted", {
1604
- kind: "aborted",
1605
- retryable: false,
1606
- provider: "xai"
1607
- });
1608
- }
1609
- throw classifyStoreError(e);
1610
- }
1611
- if (!res.ok) {
3220
+ return this.bounded(signal, async (bound) => {
3221
+ let res;
1612
3222
  try {
1613
- await throwHttpFailure(res);
3223
+ res = await this.fetchImpl(url, this.requestInit("GET", { signal: bound }));
1614
3224
  } catch (e) {
3225
+ if (bound.aborted) throw abortedError(bound, "xAI file list aborted");
1615
3226
  throw classifyStoreError(e);
1616
3227
  }
1617
- }
1618
- const json = await res.json();
1619
- const files = Array.isArray(json.data) ? json.data.map((f) => makeHandle(f)) : [];
1620
- const result = { files };
1621
- if (typeof json.pagination_token === "string" && json.pagination_token.length > 0) {
1622
- result.paginationToken = json.pagination_token;
1623
- }
1624
- 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
+ });
1625
3243
  }
1626
3244
  /**
1627
3245
  * Delete a file. Idempotent: HTTP 404 → success.
@@ -1637,34 +3255,31 @@ var XaiFileStore = class {
1637
3255
  if (typeof fileId !== "string" || fileId.trim() === "") {
1638
3256
  throw badRequest("fileId must be a non-empty string.");
1639
3257
  }
3258
+ const url = this.filesUrl(pathSegment(fileId));
1640
3259
  const failClosed = opts?.failClosed === true;
1641
- const signal = opts?.signal;
1642
- try {
1643
- const res = await this.fetchImpl(
1644
- this.filesUrl(fileId),
1645
- this.requestInit("DELETE", signal !== void 0 ? { signal } : {})
1646
- );
1647
- if (res.status === 404) {
1648
- return;
1649
- }
1650
- if (!res.ok) {
1651
- await throwHttpFailure(res);
1652
- }
1653
- } catch (err) {
1654
- if (isNotFoundError(err)) {
1655
- return;
1656
- }
1657
- const classified = signal?.aborted === true && !(err instanceof core.LlmError) ? new core.LlmError("xAI file delete aborted", {
1658
- kind: "aborted",
1659
- retryable: false,
1660
- provider: "xai",
1661
- cause: err
1662
- }) : classifyStoreError(err);
1663
- if (failClosed) {
1664
- 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);
1665
3281
  }
1666
- this.onDeleteError(fileId, classified);
1667
- }
3282
+ });
1668
3283
  }
1669
3284
  /**
1670
3285
  * Delete many files.
@@ -1686,36 +3301,48 @@ var XaiFileStore = class {
1686
3301
  if (typeof fileId !== "string" || fileId.trim() === "") {
1687
3302
  throw badRequest("fileId must be a non-empty string.");
1688
3303
  }
1689
- let res;
1690
- try {
1691
- res = await this.fetchImpl(
1692
- this.filesUrl(`${fileId}/content`),
1693
- this.requestInit("GET", signal !== void 0 ? { signal } : {})
1694
- );
1695
- } catch (e) {
1696
- if (signal?.aborted === true) {
1697
- throw new core.LlmError("xAI file content download aborted", {
1698
- kind: "aborted",
1699
- retryable: false,
1700
- provider: "xai"
1701
- });
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
+ }
1702
3322
  }
1703
- throw classifyStoreError(e);
1704
- }
1705
- if (res.status === 404) {
1706
- throw notFoundError(fileId, "getContent");
1707
- }
1708
- if (!res.ok) {
1709
3323
  try {
1710
- await throwHttpFailure(res);
3324
+ return new Uint8Array(await res.arrayBuffer());
1711
3325
  } catch (e) {
3326
+ if (bound.aborted)
3327
+ throw abortedError(bound, "xAI file content download aborted", e);
1712
3328
  throw classifyStoreError(e);
1713
3329
  }
1714
- }
1715
- const buf = await res.arrayBuffer();
1716
- return new Uint8Array(buf);
3330
+ });
1717
3331
  }
1718
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
+ }
1719
3346
 
1720
3347
  // src/provider.ts
1721
3348
  function xaiProvider(opts) {
@@ -1729,11 +3356,15 @@ function xaiProvider(opts) {
1729
3356
  exports.Grok45ConfigSchema = Grok45ConfigSchema;
1730
3357
  exports.Grok46ConfigSchema = Grok46ConfigSchema;
1731
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;
1732
3361
  exports.XAI_FILES_DEFAULT_BASE_URL = XAI_FILES_DEFAULT_BASE_URL;
3362
+ exports.XAI_FILES_DEFAULT_TIMEOUT_MS = XAI_FILES_DEFAULT_TIMEOUT_MS;
1733
3363
  exports.XAI_FILE_MAX_BYTES = XAI_FILE_MAX_BYTES;
1734
3364
  exports.XAI_FILE_TTL_MAX_SECONDS = XAI_FILE_TTL_MAX_SECONDS;
1735
3365
  exports.XAI_FILE_TTL_MIN_SECONDS = XAI_FILE_TTL_MIN_SECONDS;
1736
3366
  exports.XAI_PRICING = XAI_PRICING;
3367
+ exports.XAI_TIMEOUT_BUFFER_MS = XAI_TIMEOUT_BUFFER_MS;
1737
3368
  exports.XAI_TOOL_RATE_MICRO_USD = XAI_TOOL_RATE_MICRO_USD;
1738
3369
  exports.XaiFileStore = XaiFileStore;
1739
3370
  exports.buildXaiClient = buildXaiClient;