@gullabs/xai 0.9.0 → 0.16.0

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