@gullabs/xai 0.8.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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",
@@ -115,7 +911,31 @@ var XaiProviderOptionsSchema = z.strictObject({
115
911
  parallelToolCalls: z.boolean().optional().meta({
116
912
  title: "Parallel Tool Calls",
117
913
  description: "xAI Responses parallel_tool_calls. Not a generic contract field."
118
- })
914
+ }),
915
+ toolChoice: z.enum(["auto", "required", "none"]).optional().meta({
916
+ title: "Server Tool Choice",
917
+ description: "xAI Responses `tool_choice` for the server-side search tools; `required` forces at least one search, `none` disables them. Requires `tools`. Cannot be combined with function tools, file attachments or the request-level toolChoice."
918
+ }),
919
+ maxTurns: z.number().int().min(1).optional().meta({
920
+ title: "Max Turns",
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."
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");
119
939
  }).meta({
120
940
  title: "xAI Provider Options",
121
941
  description: "Allowlisted xAI provider options."
@@ -123,17 +943,17 @@ var XaiProviderOptionsSchema = z.strictObject({
123
943
 
124
944
  // src/model-config/grok-4-5.ts
125
945
  var Grok45ConfigSchema = z.strictObject({
126
- temperature: z.number().optional().meta({
946
+ temperature: z.number().min(0).max(2).optional().meta({
127
947
  title: "Temperature",
128
- 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.'
129
949
  }),
130
- topP: z.number().optional().meta({
950
+ topP: z.number().min(0).max(1).optional().meta({
131
951
  title: "Top P",
132
- 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."
133
953
  }),
134
- maxOutputTokens: z.number().int().positive().optional().meta({
954
+ maxOutputTokens: maxOutputTokensSchema(XAI_MODEL_LIMITS["grok-4.5"]).optional().meta({
135
955
  title: "Max Output Tokens",
136
- 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."
137
957
  }),
138
958
  reasoning: z.strictObject({
139
959
  effort: z.enum(["low", "medium", "high"]).meta({
@@ -148,9 +968,9 @@ var Grok45ConfigSchema = z.strictObject({
148
968
  title: "Service Tier",
149
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."
150
970
  }),
151
- timeoutMs: z.number().int().positive().optional().meta({
971
+ timeoutMs: z.number().int().positive().max(XAI_MAX_TIMEOUT_MS).optional().meta({
152
972
  title: "Timeout",
153
- description: "Logical request timeout in milliseconds."
973
+ description: "Logical request timeout in milliseconds (at most 2147478647)."
154
974
  }),
155
975
  providerOptions: z.strictObject({
156
976
  xai: XaiProviderOptionsSchema.optional()
@@ -164,17 +984,17 @@ var Grok45ConfigSchema = z.strictObject({
164
984
  examples: [{ reasoning: { effort: "high" } }]
165
985
  });
166
986
  var Grok46ConfigSchema = z.strictObject({
167
- temperature: z.number().optional().meta({
987
+ temperature: z.number().min(0).max(2).optional().meta({
168
988
  title: "Temperature",
169
- 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.'
170
990
  }),
171
- topP: z.number().optional().meta({
991
+ topP: z.number().min(0).max(1).optional().meta({
172
992
  title: "Top P",
173
- 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."
174
994
  }),
175
- maxOutputTokens: z.number().int().positive().optional().meta({
995
+ maxOutputTokens: maxOutputTokensSchema(XAI_MODEL_LIMITS["grok-4.6"]).optional().meta({
176
996
  title: "Max Output Tokens",
177
- 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."
178
998
  }),
179
999
  reasoning: z.strictObject({
180
1000
  effort: z.enum(["low", "medium", "high", "xhigh"]).meta({
@@ -189,9 +1009,9 @@ var Grok46ConfigSchema = z.strictObject({
189
1009
  title: "Service Tier",
190
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.'
191
1011
  }),
192
- timeoutMs: z.number().int().positive().optional().meta({
1012
+ timeoutMs: z.number().int().positive().max(XAI_MAX_TIMEOUT_MS).optional().meta({
193
1013
  title: "Timeout",
194
- description: "Logical request timeout in milliseconds."
1014
+ description: "Logical request timeout in milliseconds (at most 2147478647)."
195
1015
  }),
196
1016
  providerOptions: z.strictObject({
197
1017
  xai: XaiProviderOptionsSchema.optional()
@@ -205,17 +1025,17 @@ var Grok46ConfigSchema = z.strictObject({
205
1025
  examples: [{ reasoning: { effort: "high" } }]
206
1026
  });
207
1027
  var Grok47ConfigSchema = z.strictObject({
208
- temperature: z.number().optional().meta({
1028
+ temperature: z.number().min(0).max(2).optional().meta({
209
1029
  title: "Temperature",
210
- 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.'
211
1031
  }),
212
- topP: z.number().optional().meta({
1032
+ topP: z.number().min(0).max(1).optional().meta({
213
1033
  title: "Top P",
214
- 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."
215
1035
  }),
216
- maxOutputTokens: z.number().int().positive().optional().meta({
1036
+ maxOutputTokens: maxOutputTokensSchema(XAI_MODEL_LIMITS["grok-4.7"]).optional().meta({
217
1037
  title: "Max Output Tokens",
218
- 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."
219
1039
  }),
220
1040
  reasoning: z.strictObject({
221
1041
  effort: z.enum(["low", "medium", "high", "xhigh"]).meta({
@@ -230,9 +1050,9 @@ var Grok47ConfigSchema = z.strictObject({
230
1050
  title: "Service Tier",
231
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.'
232
1052
  }),
233
- timeoutMs: z.number().int().positive().optional().meta({
1053
+ timeoutMs: z.number().int().positive().max(XAI_MAX_TIMEOUT_MS).optional().meta({
234
1054
  title: "Timeout",
235
- description: "Logical request timeout in milliseconds."
1055
+ description: "Logical request timeout in milliseconds (at most 2147478647)."
236
1056
  }),
237
1057
  providerOptions: z.strictObject({
238
1058
  xai: XaiProviderOptionsSchema.optional()
@@ -250,37 +1070,39 @@ var Grok47ConfigSchema = z.strictObject({
250
1070
  var grok45ModelDescriptor = {
251
1071
  model: "grok-4.5",
252
1072
  provider: "xai",
1073
+ limits: XAI_MODEL_LIMITS["grok-4.5"],
253
1074
  pricingFamily: "grok-4.5",
254
1075
  capabilities: {
1076
+ inputMimeTypes: XAI_INPUT_MIME_TYPES,
255
1077
  reasoning: true,
256
1078
  reasoningApi: "level",
257
1079
  admittedReasoningEfforts: ["low", "medium", "high"],
258
1080
  structuredOutput: true,
259
1081
  nativeStructuredOutput: true,
260
- vision: true,
261
- audioInput: false,
262
1082
  sampling: "tunable",
263
1083
  caching: { explicit: false, minTokens: 0 },
264
1084
  grounding: true,
1085
+ structuredOutputWithTools: true,
265
1086
  functionCalling: true,
266
1087
  serviceTiers: ["priority"]
267
1088
  },
268
1089
  configSchema: Grok45ConfigSchema,
1090
+ configKeys: toConfigKeys(Grok45ConfigSchema),
269
1091
  configJsonSchema: toConfigJsonSchema(Grok45ConfigSchema),
270
1092
  validateConfig: zodToStandardSchema(Grok45ConfigSchema)
271
1093
  };
272
1094
  var grok46ModelDescriptor = {
273
1095
  model: "grok-4.6",
274
1096
  provider: "xai",
1097
+ limits: XAI_MODEL_LIMITS["grok-4.6"],
275
1098
  pricingFamily: "grok-4.6",
276
1099
  capabilities: {
1100
+ inputMimeTypes: XAI_INPUT_MIME_TYPES,
277
1101
  reasoning: true,
278
1102
  reasoningApi: "level",
279
1103
  admittedReasoningEfforts: ["low", "medium", "high", "xhigh"],
280
1104
  structuredOutput: true,
281
1105
  nativeStructuredOutput: true,
282
- vision: true,
283
- audioInput: false,
284
1106
  sampling: "tunable",
285
1107
  caching: { explicit: false, minTokens: 0 },
286
1108
  grounding: true,
@@ -289,29 +1111,33 @@ var grok46ModelDescriptor = {
289
1111
  serviceTiers: ["priority"]
290
1112
  },
291
1113
  configSchema: Grok46ConfigSchema,
1114
+ configKeys: toConfigKeys(Grok46ConfigSchema),
292
1115
  configJsonSchema: toConfigJsonSchema(Grok46ConfigSchema),
293
1116
  validateConfig: zodToStandardSchema(Grok46ConfigSchema)
294
1117
  };
295
1118
  var grok47ModelDescriptor = {
296
1119
  model: "grok-4.7",
297
1120
  provider: "xai",
1121
+ limits: XAI_MODEL_LIMITS["grok-4.7"],
298
1122
  pricingFamily: "grok-4.7",
299
1123
  capabilities: {
1124
+ inputMimeTypes: XAI_INPUT_MIME_TYPES,
300
1125
  reasoning: true,
301
1126
  reasoningApi: "level",
302
1127
  admittedReasoningEfforts: ["low", "medium", "high", "xhigh"],
303
1128
  structuredOutput: true,
304
1129
  nativeStructuredOutput: true,
305
- vision: true,
306
- audioInput: false,
307
1130
  sampling: "tunable",
308
1131
  caching: { explicit: false, minTokens: 0 },
309
1132
  grounding: true,
1133
+ structuredOutputWithTools: true,
310
1134
  functionCalling: true,
311
- statelessReasoningReplay: true,
1135
+ continuation: "state",
1136
+ providerState: true,
312
1137
  serviceTiers: ["priority"]
313
1138
  },
314
1139
  configSchema: Grok47ConfigSchema,
1140
+ configKeys: toConfigKeys(Grok47ConfigSchema),
315
1141
  configJsonSchema: toConfigJsonSchema(Grok47ConfigSchema),
316
1142
  validateConfig: zodToStandardSchema(Grok47ConfigSchema)
317
1143
  };
@@ -321,6 +1147,48 @@ var xaiModelDescriptors = [
321
1147
  grok47ModelDescriptor
322
1148
  ];
323
1149
  var xaiRegistry = createModelRegistry(xaiModelDescriptors);
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
+ };
324
1192
  var xaiPricingVersion = "xai-2026-09-25";
325
1193
  var XAI_TOOL_RATE_MICRO_USD = {
326
1194
  web_search_calls: 5e3,
@@ -334,6 +1202,51 @@ var XAI_TOOL_COUNTER_KEYS = [
334
1202
  ];
335
1203
  var X_SEARCH_ITEM_COUNTERS = ["x_posts_fetched", "x_users_fetched"];
336
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
+ }
337
1250
  var XAI_PRICING = Object.freeze({
338
1251
  // ── grok-4.5 ── $2.00/$6.00 (<200k), $4.00/$12.00 (>=200k); cached $0.30/$0.60
339
1252
  "grok-4.5": {
@@ -411,6 +1324,11 @@ function scaleRates(rates, factor) {
411
1324
  return scaled;
412
1325
  }
413
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) {
414
1332
  const listed = lookupConcreteRates(model, tier);
415
1333
  if (listed === void 0) {
416
1334
  return computeCost(model, usage, tier, lookupConcreteRates, xaiPricingVersion);
@@ -446,11 +1364,12 @@ function computeXaiCost(model, usage, tier) {
446
1364
  return sum + Math.round(count * XAI_TOOL_RATE_MICRO_USD[key]);
447
1365
  }, 0);
448
1366
  const microUsd = inputCost + cachedCost + outputCost + toolsCost;
1367
+ const unpricedCounters = unpricedXaiToolCounters(usage);
449
1368
  return {
450
1369
  microUsd,
451
1370
  usd: microUsd / 1e6,
452
1371
  pricingVersion: xaiPricingVersion,
453
- confidence: missingWebCounter || attachmentUnpinned ? "estimated" : "exact",
1372
+ confidence: missingWebCounter || attachmentUnpinned || unpricedCounters.length > 0 || usage.details[XAI_ESTIMATED_USAGE_KEY] === 1 ? "estimated" : "exact",
454
1373
  details: {
455
1374
  input: inputCost,
456
1375
  cached: cachedCost,
@@ -484,22 +1403,42 @@ function isXaiMessageItem(item) {
484
1403
  function isXaiReasoningItem(item) {
485
1404
  return item.type === "reasoning" && Array.isArray(item.summary);
486
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
+ }
487
1412
  function badXaiRequest(message) {
488
1413
  return new LlmError(message, { kind: "bad_request", retryable: false });
489
1414
  }
490
- var ALLOWED_XAI_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/jpg", "image/png"]);
491
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
+ }
492
1436
  function mapPart(p) {
493
1437
  switch (p.kind) {
494
1438
  case "text":
495
1439
  return { type: "input_text", text: p.text };
496
1440
  case "inline-media": {
497
- if (!ALLOWED_XAI_IMAGE_MIME_TYPES.has(p.mimeType)) {
498
- throw badXaiRequest(
499
- `xAI vision only supports image/jpeg and image/png; got mimeType "${p.mimeType}".`
500
- );
501
- }
502
- const byteLength = Buffer.from(p.data, "base64").length;
1441
+ const byteLength = decodedBase64Length(p.data);
503
1442
  if (byteLength > MAX_XAI_INLINE_IMAGE_BYTES) {
504
1443
  throw badXaiRequest(
505
1444
  `xAI inline images must be at most 20 MiB; got ${byteLength} bytes.`
@@ -509,8 +1448,7 @@ function mapPart(p) {
509
1448
  }
510
1449
  case "file-uri": {
511
1450
  const isPublicHttpUrl = p.uri.startsWith("http://") || p.uri.startsWith("https://");
512
- const isAllowedImageType = ALLOWED_XAI_IMAGE_MIME_TYPES.has(p.mimeType);
513
- if (!isPublicHttpUrl || !isAllowedImageType) {
1451
+ if (!isPublicHttpUrl) {
514
1452
  throw badXaiRequest(
515
1453
  `xAI only accepts public http(s) image URLs via FileUriPart; got scheme of "${p.uri}" / mimeType "${p.mimeType}".`
516
1454
  );
@@ -537,7 +1475,80 @@ function mapPart(p) {
537
1475
  return assertNever(p);
538
1476
  }
539
1477
  }
540
- var XAI_PROVIDER_OPTION_KEYS = /* @__PURE__ */ new Set(["promptCacheKey", "tools", "parallelToolCalls"]);
1478
+ var XAI_PROVIDER_OPTION_KEYS = /* @__PURE__ */ new Set([
1479
+ "promptCacheKey",
1480
+ "tools",
1481
+ "parallelToolCalls",
1482
+ "toolChoice",
1483
+ "maxTurns",
1484
+ "searchBudget"
1485
+ ]);
1486
+ var XAI_DEFAULT_SCHEMA_NAME = "structured_output";
1487
+ var XAI_SCHEMA_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
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
+ }
541
1552
  function mapXaiProviderOptions(xaiOpts, model) {
542
1553
  if (xaiOpts === void 0) {
543
1554
  return {};
@@ -552,7 +1563,7 @@ function mapXaiProviderOptions(xaiOpts, model) {
552
1563
  throw badXaiRequest(
553
1564
  `providerOptions.xai contains unsupported keys [${unknownKeys.join(
554
1565
  ", "
555
- )}] for model "${model}". Allowed keys: promptCacheKey, tools, parallelToolCalls.`
1566
+ )}] for model "${model}". Allowed keys: promptCacheKey, tools, parallelToolCalls, toolChoice, maxTurns, searchBudget.`
556
1567
  );
557
1568
  }
558
1569
  const mapped = {};
@@ -575,15 +1586,53 @@ function mapXaiProviderOptions(xaiOpts, model) {
575
1586
  }
576
1587
  mapped.parallelToolCalls = xaiOpts["parallelToolCalls"];
577
1588
  }
1589
+ const toolChoice = xaiOpts["toolChoice"];
1590
+ if (toolChoice !== void 0) {
1591
+ if (typeof toolChoice !== "string" || !XAI_SERVER_TOOL_CHOICES.has(toolChoice)) {
1592
+ throw badXaiRequest(
1593
+ `providerOptions.xai.toolChoice must be "auto", "required" or "none" for model "${model}".`
1594
+ );
1595
+ }
1596
+ if (mapped.tools === void 0 || mapped.tools.length === 0) {
1597
+ throw badXaiRequest(
1598
+ `providerOptions.xai.toolChoice requires a non-empty providerOptions.xai.tools for model "${model}".`
1599
+ );
1600
+ }
1601
+ mapped.toolChoice = toolChoice;
1602
+ }
1603
+ const maxTurns = xaiOpts["maxTurns"];
1604
+ if (maxTurns !== void 0) {
1605
+ if (typeof maxTurns !== "number" || !Number.isInteger(maxTurns) || maxTurns < 1) {
1606
+ throw badXaiRequest(
1607
+ `providerOptions.xai.maxTurns must be an integer >= 1 for model "${model}".`
1608
+ );
1609
+ }
1610
+ if (mapped.tools === void 0 || mapped.tools.length === 0) {
1611
+ throw badXaiRequest(
1612
+ `providerOptions.xai.maxTurns requires a non-empty providerOptions.xai.tools for model "${model}".`
1613
+ );
1614
+ }
1615
+ mapped.maxTurns = maxTurns;
1616
+ }
1617
+ if (xaiOpts["searchBudget"] !== void 0) {
1618
+ mapped.searchBudget = mapXaiSearchBudget(xaiOpts["searchBudget"], mapped.tools, model);
1619
+ }
578
1620
  return mapped;
579
1621
  }
580
1622
  function parseXaiReplayState(value, model) {
581
1623
  if (value === void 0) return void 0;
582
- 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(
583
1632
  (item) => !isPlainRecord(item) || typeof item["type"] !== "string" && typeof item["role"] !== "string"
584
1633
  )) {
585
1634
  throw badXaiRequest(
586
- `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}".`
587
1636
  );
588
1637
  }
589
1638
  return value;
@@ -717,35 +1766,56 @@ function isXaiSafetyCheckBody(rawErr) {
717
1766
  const text = extractXaiErrorBodyText(rawErr);
718
1767
  return text !== void 0 && text.startsWith(XAI_SAFETY_CHECK_MESSAGE_PREFIX);
719
1768
  }
720
- var XAI_TRANSPORT_ERROR_PATTERN = /connection error|econnreset|econnrefused|etimedout|eai_again|epipe|socket hang up|fetch failed/i;
721
- function matchesXaiTransportSignature(err) {
722
- if (!(err instanceof Error)) return false;
723
- if (XAI_TRANSPORT_ERROR_PATTERN.test(err.message)) return true;
724
- const code = err.code;
725
- return typeof code === "string" && XAI_TRANSPORT_ERROR_PATTERN.test(code);
726
- }
727
- function isXaiTransportError(rawErr) {
728
- if (!(rawErr instanceof Error)) return false;
729
- const ctorName = rawErr.constructor.name;
730
- if (ctorName === "APIConnectionError" || ctorName === "APIConnectionTimeoutError") {
731
- return true;
732
- }
733
- if (matchesXaiTransportSignature(rawErr)) return true;
734
- const cause = rawErr.cause;
735
- if (cause instanceof Error) {
736
- const causeCtorName = cause.constructor.name;
737
- if (causeCtorName === "APIConnectionError" || causeCtorName === "APIConnectionTimeoutError") {
738
- return true;
739
- }
740
- if (matchesXaiTransportSignature(cause)) return true;
741
- }
742
- 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");
743
1776
  }
744
- 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) {
745
1801
  if (rawErr instanceof LlmError) {
746
1802
  return rawErr;
747
1803
  }
1804
+ if (rawErr instanceof XaiStreamError) {
1805
+ return classifyStreamError(rawErr, estimatedInputTokens, requestTimeoutMs);
1806
+ }
748
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
+ }
749
1819
  if (base.httpStatus === 400 && isXaiAuthFailureBody(rawErr)) {
750
1820
  return new LlmError(base.message, {
751
1821
  kind: "invalid_auth",
@@ -765,7 +1835,23 @@ function classifyXaiError(rawErr) {
765
1835
  cause: base.cause ?? rawErr
766
1836
  });
767
1837
  }
768
- if (base.kind === "unknown" && isXaiTransportError(rawErr)) {
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)) {
769
1855
  return new LlmError(base.message, {
770
1856
  kind: "server",
771
1857
  retryable: true,
@@ -773,16 +1859,233 @@ function classifyXaiError(rawErr) {
773
1859
  cause: base.cause ?? rawErr
774
1860
  });
775
1861
  }
776
- return new LlmError(base.message, {
777
- kind: base.kind,
778
- retryable: base.retryable,
779
- ...base.httpStatus !== void 0 ? { httpStatus: base.httpStatus } : {},
780
- ...base.retryAfterMs !== void 0 ? { retryAfterMs: base.retryAfterMs } : {},
781
- provider: "xai",
782
- cause: base.cause ?? rawErr
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,
2002
+ provider: "xai",
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;
2025
+ }
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"
783
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
+ };
784
2070
  }
785
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
+ }
786
2089
  return {
787
2090
  id: "xai",
788
2091
  async run(req, ctx) {
@@ -794,12 +2097,16 @@ function xaiAdapter(opts) {
794
2097
  }
795
2098
  const warnings = [];
796
2099
  const model = req.model;
797
- if (req.modelDescriptor !== void 0 && (req.modelDescriptor.model !== model || req.modelDescriptor.provider !== "xai")) {
798
- 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");
799
2106
  }
800
- 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") {
801
2108
  throw badXaiRequest(
802
- `A matching xAI model descriptor with statelessReasoningReplay is required for "${model}".`
2109
+ `A matching xAI model descriptor with continuation "state" is required for "${model}".`
803
2110
  );
804
2111
  }
805
2112
  const genConfig = req.config;
@@ -807,11 +2114,11 @@ function xaiAdapter(opts) {
807
2114
  genConfig.providerOptions?.["xai"],
808
2115
  model
809
2116
  );
810
- const replayRequired = req.modelDescriptor?.capabilities?.statelessReasoningReplay === true;
2117
+ const replayRequired = req.modelDescriptor?.capabilities?.continuation === "state";
811
2118
  const replayState = parseXaiReplayState(req.transientProviderState, model);
812
2119
  if (replayState !== void 0 && !replayRequired) {
813
2120
  throw badXaiRequest(
814
- `transientProviderState requires a statelessReasoningReplay model descriptor for "${model}".`
2121
+ `transientProviderState requires a model descriptor with continuation "state" for "${model}".`
815
2122
  );
816
2123
  }
817
2124
  if (replayState !== void 0 && req.messages.length === 0) {
@@ -819,12 +2126,12 @@ function xaiAdapter(opts) {
819
2126
  `Stateless conversation replay for model "${model}" requires new messages to append.`
820
2127
  );
821
2128
  }
822
- const input = [...replayState?.input ?? []];
2129
+ const input = [...replayState?.xai.input ?? []];
823
2130
  const replayCallIds = new Set(
824
- 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") ?? []
825
2132
  );
826
2133
  const replayedResultIds = new Set(
827
- replayState?.input.filter(
2134
+ replayState?.xai.input.filter(
828
2135
  (item) => isPlainRecord(item) && item["type"] === "function_call_output"
829
2136
  ).map((item) => isPlainRecord(item) ? item["call_id"] : void 0).filter((id) => typeof id === "string") ?? []
830
2137
  );
@@ -944,7 +2251,18 @@ function xaiAdapter(opts) {
944
2251
  const structuredOutputRequested = req.outputJsonSchema !== void 0;
945
2252
  if (structuredOutputRequested) {
946
2253
  const schema = req.outputJsonSchema;
947
- 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;
948
2266
  params.text = {
949
2267
  format: { type: "json_schema", name, schema, strict: true }
950
2268
  };
@@ -970,6 +2288,27 @@ function xaiAdapter(opts) {
970
2288
  );
971
2289
  }
972
2290
  params.tools = searchTools;
2291
+ if (xaiProviderConfig.toolChoice !== void 0) {
2292
+ if (req.toolChoice !== void 0) {
2293
+ throw badXaiRequest(
2294
+ `providerOptions.xai.toolChoice and toolChoice cannot both be set for model "${model}"; xAI accepts one tool_choice per request.`
2295
+ );
2296
+ }
2297
+ if (req.tools !== void 0 && req.tools.length > 0) {
2298
+ throw badXaiRequest(
2299
+ `providerOptions.xai.toolChoice applies to the server-side search tools only and cannot be combined with function tools for model "${model}".`
2300
+ );
2301
+ }
2302
+ if (hasFileRef) {
2303
+ throw badXaiRequest(
2304
+ `providerOptions.xai.toolChoice cannot be combined with file attachments for model "${model}"; xAI's implicit attachment_search would count as the tool call.`
2305
+ );
2306
+ }
2307
+ params.tool_choice = xaiProviderConfig.toolChoice;
2308
+ }
2309
+ if (xaiProviderConfig.maxTurns !== void 0) {
2310
+ params.max_turns = xaiProviderConfig.maxTurns;
2311
+ }
973
2312
  }
974
2313
  if (req.tools !== void 0 && req.tools.length > 0) {
975
2314
  if (req.modelDescriptor?.capabilities?.functionCalling !== true) {
@@ -977,6 +2316,13 @@ function xaiAdapter(opts) {
977
2316
  `tools is not supported for xai model "${model}" (capabilities.functionCalling is not true).`
978
2317
  );
979
2318
  }
2319
+ req.tools.forEach((tool, index) => {
2320
+ assertJsonSchemaProfile(
2321
+ tool.inputJsonSchema,
2322
+ `tools[${index}].inputJsonSchema`,
2323
+ XAI_JSON_SCHEMA_PROFILE
2324
+ );
2325
+ });
980
2326
  const functionTools = req.tools.map((tool) => ({
981
2327
  type: "function",
982
2328
  name: tool.name,
@@ -989,129 +2335,300 @@ function xaiAdapter(opts) {
989
2335
  }
990
2336
  }
991
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
+ }
992
2343
  params.parallel_tool_calls = xaiProviderConfig.parallelToolCalls;
993
2344
  }
994
2345
  let response;
2346
+ let responseMeta;
2347
+ let sdkCallStart;
995
2348
  try {
996
- const buildClient = opts?._clientFactory ?? buildXaiClient;
997
- 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);
998
2351
  ctx.logger.debug(
999
2352
  { model, configKeys: Object.keys(params) },
1000
2353
  "llm.adapter.dispatch"
1001
2354
  );
1002
- response = await client.responses.create(
1003
- params,
1004
- ctx.signal !== void 0 ? { signal: ctx.signal } : void 0
1005
- );
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);
1006
2365
  } catch (rawErr) {
1007
- 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
+ );
1008
2389
  }
1009
- let text = "";
1010
- let reasoningText;
1011
- const messageItems = [];
1012
- const toolCalls = [];
1013
- for (const item of response.output) {
1014
- if (isXaiMessageItem(item)) {
1015
- messageItems.push(item);
1016
- } else if (isXaiReasoningItem(item)) {
1017
- const joined = item.summary.map((s) => s.text).join("");
1018
- if (joined.length > 0) {
1019
- reasoningText = (reasoningText ?? "") + joined;
2390
+ for (const message of responseMeta?.streamNotes ?? []) {
2391
+ warnings.push({ type: "other", message });
2392
+ }
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
+ }
1020
2443
  }
1021
- } else if (item.type === "function_call") {
1022
- const callId = typeof item["call_id"] === "string" ? item["call_id"] : "";
1023
- const name = typeof item["name"] === "string" ? item["name"] : "";
1024
- let args = {};
1025
- if (typeof item["arguments"] === "string") {
1026
- try {
1027
- args = JSON.parse(item["arguments"]);
1028
- } catch {
1029
- 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);
1030
2460
  }
1031
2461
  }
1032
- if (callId.length > 0 && name.length > 0) {
1033
- 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
+ });
1034
2473
  }
1035
2474
  }
1036
- }
1037
- if (messageItems.length > 0) {
1038
- const lastMessage = messageItems[messageItems.length - 1];
1039
- text = lastMessage.content.map((part) => part.text).join("");
1040
- 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";
1041
2495
  warnings.push({
1042
2496
  type: "other",
1043
- 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}".`
1044
2498
  });
1045
2499
  }
1046
- }
1047
- let rawStructured;
1048
- if (structuredOutputRequested && text.length > 0) {
1049
- try {
1050
- rawStructured = JSON.parse(text);
1051
- } 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
+ });
1052
2507
  }
1053
- }
1054
- const usage = mapUsage(response.usage);
1055
- const finishReason = mapFinishReason(response);
1056
- const expectedToolCounters = expectedServerToolCounters(
1057
- xaiProviderConfig.tools);
1058
- if (expectedToolCounters.length > 0 || hasFileRef) {
1059
- usage.details["server_tools_requested"] = 1;
1060
- if (xaiProviderConfig.tools?.some((tool) => tool["type"] === "x_search") === true) {
1061
- 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
+ }
1062
2523
  }
1063
- const missing = expectedToolCounters.filter((key) => !(key in usage.details));
1064
- if (missing.length > 0) {
1065
- 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) {
1066
2528
  warnings.push({
1067
2529
  type: "other",
1068
- message: `xai: server tools were requested but usage is missing counters [${missing.join(
1069
- ", "
1070
- )}]; 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.`
1071
2531
  });
1072
2532
  }
1073
- if (hasFileRef) {
1074
- 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;
1075
2537
  warnings.push({
1076
2538
  type: "other",
1077
- 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.`
1078
2540
  });
1079
2541
  }
1080
- }
1081
- const citations = collectXaiCitations(response, messageItems);
1082
- const providerMeta = {};
1083
- const contextDetails = response.usage["context_details"];
1084
- if (isPlainRecord(contextDetails)) {
1085
- providerMeta["context_details"] = contextDetails;
1086
- }
1087
- if (isPlainRecord(response.metadata)) {
1088
- providerMeta["metadata"] = response.metadata;
1089
- }
1090
- let transientProviderState;
1091
- if (replayRequired) {
1092
- const state = {
1093
- model,
1094
- 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" } : {}
1095
2616
  };
1096
- transientProviderState = state;
1097
- }
1098
- const servedServiceTier = typeof response.service_tier === "string" && response.service_tier.length > 0 ? response.service_tier : void 0;
1099
- const result = {
1100
- model: response.model,
1101
- usage,
1102
- warnings,
1103
- finishReason,
1104
- responseId: response.id,
1105
- ...text.length > 0 ? { text } : {},
1106
- ...reasoningText !== void 0 ? { reasoningText } : {},
1107
- ...rawStructured !== void 0 ? { rawStructured } : {},
1108
- ...servedServiceTier !== void 0 ? { servedServiceTier } : {},
1109
- ...Object.keys(providerMeta).length > 0 ? { providerMetadata: providerMeta } : {},
1110
- ...transientProviderState !== void 0 ? { transientProviderState } : {},
1111
- ...citations.length > 0 ? { citations } : {},
1112
- ...toolCalls.length > 0 ? { toolCalls, finishReason: "tool_calls" } : {}
1113
- };
1114
- 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
+ }
1115
2632
  },
1116
2633
  async countTokens(req, ctx) {
1117
2634
  if (req.provider !== "xai") {
@@ -1127,16 +2644,32 @@ function xaiAdapter(opts) {
1127
2644
  }
1128
2645
  const text = concatenateTokenizeText(req);
1129
2646
  const apiKey = requireApiKey(ctx.auth);
1130
- 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);
1131
2661
  try {
1132
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,
1133
2666
  method: "POST",
1134
2667
  headers: {
1135
2668
  Authorization: `Bearer ${apiKey}`,
1136
2669
  "Content-Type": "application/json"
1137
2670
  },
1138
2671
  body: JSON.stringify({ model: req.model, text }),
1139
- ...ctx.signal !== void 0 ? { signal: ctx.signal } : {}
2672
+ signal: controller.signal
1140
2673
  });
1141
2674
  if (!res.ok) {
1142
2675
  let parsed;
@@ -1145,12 +2678,26 @@ function xaiAdapter(opts) {
1145
2678
  } catch {
1146
2679
  parsed = await res.text().catch(() => "");
1147
2680
  }
1148
- throw Object.assign(new Error(`xAI tokenize-text HTTP ${res.status}`), {
1149
- status: res.status,
1150
- 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
1151
2699
  });
1152
2700
  }
1153
- const raw = await res.json();
1154
2701
  if (!isPlainRecord(raw) || !Array.isArray(raw["token_ids"])) {
1155
2702
  throw new LlmError(
1156
2703
  "xAI tokenize-text response is malformed: missing required field: token_ids",
@@ -1165,6 +2712,9 @@ function xaiAdapter(opts) {
1165
2712
  raw
1166
2713
  };
1167
2714
  } catch (rawErr) {
2715
+ if (controller.signal.aborted && controller.signal.reason === timeout) {
2716
+ throw timeout;
2717
+ }
1168
2718
  if (rawErr instanceof Error && rawErr.name === "AbortError") {
1169
2719
  throw new LlmError("xAI tokenize-text aborted", {
1170
2720
  kind: "aborted",
@@ -1174,12 +2724,15 @@ function xaiAdapter(opts) {
1174
2724
  });
1175
2725
  }
1176
2726
  throw classifyXaiError(rawErr);
2727
+ } finally {
2728
+ clearTimeout(timer);
2729
+ ctx.signal?.removeEventListener("abort", forwardAbort);
1177
2730
  }
1178
2731
  }
1179
2732
  };
1180
2733
  }
1181
2734
  var WEB_SEARCH_COUNTER = "web_search_calls";
1182
- function expectedServerToolCounters(tools, _hasFileRef) {
2735
+ function expectedServerToolCounters(tools) {
1183
2736
  const keys = [];
1184
2737
  if (tools !== void 0) {
1185
2738
  for (const tool of tools) {
@@ -1189,49 +2742,71 @@ function expectedServerToolCounters(tools, _hasFileRef) {
1189
2742
  }
1190
2743
  return keys;
1191
2744
  }
1192
- function collectXaiCitations(response, messageItems) {
1193
- const seen = /* @__PURE__ */ new Set();
1194
- const citations = [];
1195
- const push = (url, title) => {
1196
- if (typeof url !== "string" || url.length === 0) return;
1197
- if (seen.has(url)) return;
1198
- seen.add(url);
1199
- const citation = { url };
1200
- if (typeof title === "string" && title.length > 0 && title !== url) {
1201
- citation.title = title;
1202
- }
1203
- try {
1204
- const parsed = new URL(url);
1205
- if (parsed.hostname.length > 0) {
1206
- 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 {
1207
2762
  }
1208
- } catch {
2763
+ byUrl.set(url, citation);
1209
2764
  }
1210
- 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;
1211
2769
  };
1212
2770
  if (Array.isArray(response.citations)) {
1213
2771
  for (const item of response.citations) {
1214
2772
  if (typeof item === "string") {
1215
- push(item, void 0);
2773
+ upsert(item, void 0);
1216
2774
  } else if (isPlainRecord(item)) {
1217
- push(item["url"] ?? item["uri"], item["title"]);
2775
+ upsert(item["url"] ?? item["uri"], item["title"]);
1218
2776
  }
1219
2777
  }
1220
2778
  }
1221
2779
  const lastMessage = messageItems.at(-1);
1222
- const citationMessages = lastMessage === void 0 ? [] : [lastMessage];
1223
- for (const item of citationMessages) {
1224
- for (const part of item.content) {
1225
- const annotations = part.annotations;
1226
- if (!Array.isArray(annotations)) continue;
1227
- for (const ann of annotations) {
1228
- if (!isPlainRecord(ann)) continue;
1229
- if (ann["type"] !== void 0 && ann["type"] !== "url_citation") continue;
1230
- 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
+ }
1231
2805
  }
2806
+ partOffset += (partText(part) ?? "").length;
1232
2807
  }
1233
2808
  }
1234
- return citations;
2809
+ return [...byUrl.values()];
1235
2810
  }
1236
2811
  function concatenateTokenizeText(req) {
1237
2812
  const chunks = [];
@@ -1263,6 +2838,7 @@ var XAI_FILE_TTL_MIN_SECONDS = 3600;
1263
2838
  var XAI_FILE_TTL_MAX_SECONDS = 2592e3;
1264
2839
  var XAI_FILE_MAX_BYTES = 48 * 1024 * 1024;
1265
2840
  var XAI_FILES_DEFAULT_BASE_URL = "https://api.x.ai/v1";
2841
+ var XAI_FILES_DEFAULT_TIMEOUT_MS = 6e4;
1266
2842
  function isPlainRecord2(value) {
1267
2843
  return typeof value === "object" && value !== null && !Array.isArray(value);
1268
2844
  }
@@ -1331,19 +2907,23 @@ function toBlob(data, mimeType) {
1331
2907
  var XaiFilesHttpError = class extends Error {
1332
2908
  status;
1333
2909
  error;
1334
- constructor(status, message, errorBody) {
2910
+ /** The response headers: `classifyError` reads `Retry-After` from them. */
2911
+ headers;
2912
+ constructor(status, message, errorBody, headers) {
1335
2913
  super(message);
1336
2914
  this.name = "XaiFilesHttpError";
1337
2915
  this.status = status;
1338
2916
  this.error = errorBody;
2917
+ this.headers = headers;
1339
2918
  }
1340
2919
  };
1341
- async function throwHttpFailure(res) {
2920
+ async function throwHttpFailure(res, signal, operation) {
1342
2921
  const status = res.status;
1343
2922
  let bodyText = "";
1344
2923
  try {
1345
2924
  bodyText = await res.text();
1346
- } catch {
2925
+ } catch (e) {
2926
+ if (signal.aborted) throw abortedError(signal, `xAI file ${operation} aborted`, e);
1347
2927
  bodyText = "";
1348
2928
  }
1349
2929
  let parsed = bodyText;
@@ -1360,7 +2940,9 @@ async function throwHttpFailure(res) {
1360
2940
  } else if (isPlainRecord2(parsed) && typeof parsed["error"] === "string") {
1361
2941
  message = parsed["error"];
1362
2942
  }
1363
- 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);
1364
2946
  }
1365
2947
  function isNotFoundError(err) {
1366
2948
  if (err instanceof LlmError && err.httpStatus === 404) {
@@ -1429,16 +3011,41 @@ function classifyStoreError(raw) {
1429
3011
  cause: classified.cause ?? raw
1430
3012
  });
1431
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
+ }
1432
3031
  var XaiFileStore = class {
1433
3032
  apiKey;
1434
3033
  baseUrl;
1435
3034
  fetchImpl;
3035
+ timeoutMs;
1436
3036
  onDeleteError;
1437
3037
  logger;
1438
3038
  constructor(opts) {
1439
3039
  this.apiKey = requireApiKey(opts.auth);
1440
3040
  this.baseUrl = (opts.baseUrl ?? XAI_FILES_DEFAULT_BASE_URL).replace(/\/+$/, "");
1441
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;
1442
3049
  this.logger = opts.logger;
1443
3050
  this.onDeleteError = opts.onDeleteError ?? ((fileId, err) => {
1444
3051
  const message = redactSecrets(classifyError(err).message);
@@ -1472,6 +3079,37 @@ var XaiFileStore = class {
1472
3079
  }
1473
3080
  return init;
1474
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
+ }
1475
3113
  /**
1476
3114
  * Upload bytes to xAI Files. Returns immediately with metadata (no poll).
1477
3115
  *
@@ -1503,77 +3141,64 @@ var XaiFileStore = class {
1503
3141
  }
1504
3142
  form.append("purpose", purpose);
1505
3143
  form.append("file", toBlob(input.data, input.mimeType), input.filename);
1506
- let res;
1507
- try {
1508
- res = await this.fetchImpl(
1509
- this.filesUrl(),
1510
- this.requestInit("POST", {
1511
- body: form,
1512
- ...signal !== void 0 ? { signal } : {}
1513
- })
1514
- );
1515
- } catch (e) {
1516
- if (signal?.aborted === true) {
1517
- throw new LlmError("xAI file upload aborted", {
1518
- kind: "aborted",
1519
- retryable: false,
1520
- provider: "xai"
1521
- });
1522
- }
1523
- throw classifyStoreError(e);
1524
- }
1525
- if (!res.ok) {
3144
+ return this.bounded(signal, async (bound) => {
3145
+ let res;
1526
3146
  try {
1527
- await throwHttpFailure(res);
3147
+ res = await this.fetchImpl(
3148
+ this.filesUrl(),
3149
+ this.requestInit("POST", { body: form, signal: bound })
3150
+ );
1528
3151
  } catch (e) {
3152
+ if (bound.aborted) throw abortedError(bound, "xAI file upload aborted");
1529
3153
  throw classifyStoreError(e);
1530
3154
  }
1531
- }
1532
- let json;
1533
- try {
1534
- json = await res.json();
1535
- } catch (e) {
1536
- throw new LlmError("xAI file upload returned non-JSON body", {
1537
- kind: "server",
1538
- retryable: false,
1539
- provider: "xai",
1540
- cause: e
1541
- });
1542
- }
1543
- 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
+ });
1544
3176
  }
1545
3177
  async get(fileId, signal) {
1546
3178
  if (typeof fileId !== "string" || fileId.trim() === "") {
1547
3179
  throw badRequest("fileId must be a non-empty string.");
1548
3180
  }
1549
- let res;
1550
- try {
1551
- res = await this.fetchImpl(
1552
- this.filesUrl(fileId),
1553
- this.requestInit("GET", signal !== void 0 ? { signal } : {})
1554
- );
1555
- } catch (e) {
1556
- if (signal?.aborted === true) {
1557
- throw new LlmError("xAI file get aborted", {
1558
- kind: "aborted",
1559
- retryable: false,
1560
- provider: "xai"
1561
- });
1562
- }
1563
- throw classifyStoreError(e);
1564
- }
1565
- if (res.status === 404) {
1566
- throw notFoundError(fileId, "get");
1567
- }
1568
- if (!res.ok) {
3181
+ const url = this.filesUrl(pathSegment(fileId));
3182
+ return this.bounded(signal, async (bound) => {
3183
+ let res;
1569
3184
  try {
1570
- await throwHttpFailure(res);
3185
+ res = await this.fetchImpl(url, this.requestInit("GET", { signal: bound }));
1571
3186
  } catch (e) {
3187
+ if (bound.aborted) throw abortedError(bound, "xAI file get aborted");
1572
3188
  throw classifyStoreError(e);
1573
3189
  }
1574
- }
1575
- const json = await res.json();
1576
- 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
+ });
1577
3202
  }
1578
3203
  async list(opts = {}, signal) {
1579
3204
  if (opts.limit !== void 0) {
@@ -1590,36 +3215,29 @@ var XaiFileStore = class {
1590
3215
  }
1591
3216
  const qs = params.toString();
1592
3217
  const url = qs.length > 0 ? `${this.filesUrl()}?${qs}` : this.filesUrl();
1593
- let res;
1594
- try {
1595
- res = await this.fetchImpl(
1596
- url,
1597
- this.requestInit("GET", signal !== void 0 ? { signal } : {})
1598
- );
1599
- } catch (e) {
1600
- if (signal?.aborted === true) {
1601
- throw new LlmError("xAI file list aborted", {
1602
- kind: "aborted",
1603
- retryable: false,
1604
- provider: "xai"
1605
- });
1606
- }
1607
- throw classifyStoreError(e);
1608
- }
1609
- if (!res.ok) {
3218
+ return this.bounded(signal, async (bound) => {
3219
+ let res;
1610
3220
  try {
1611
- await throwHttpFailure(res);
3221
+ res = await this.fetchImpl(url, this.requestInit("GET", { signal: bound }));
1612
3222
  } catch (e) {
3223
+ if (bound.aborted) throw abortedError(bound, "xAI file list aborted");
1613
3224
  throw classifyStoreError(e);
1614
3225
  }
1615
- }
1616
- const json = await res.json();
1617
- const files = Array.isArray(json.data) ? json.data.map((f) => makeHandle(f)) : [];
1618
- const result = { files };
1619
- if (typeof json.pagination_token === "string" && json.pagination_token.length > 0) {
1620
- result.paginationToken = json.pagination_token;
1621
- }
1622
- 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
+ });
1623
3241
  }
1624
3242
  /**
1625
3243
  * Delete a file. Idempotent: HTTP 404 → success.
@@ -1635,34 +3253,31 @@ var XaiFileStore = class {
1635
3253
  if (typeof fileId !== "string" || fileId.trim() === "") {
1636
3254
  throw badRequest("fileId must be a non-empty string.");
1637
3255
  }
3256
+ const url = this.filesUrl(pathSegment(fileId));
1638
3257
  const failClosed = opts?.failClosed === true;
1639
- const signal = opts?.signal;
1640
- try {
1641
- const res = await this.fetchImpl(
1642
- this.filesUrl(fileId),
1643
- this.requestInit("DELETE", signal !== void 0 ? { signal } : {})
1644
- );
1645
- if (res.status === 404) {
1646
- return;
1647
- }
1648
- if (!res.ok) {
1649
- await throwHttpFailure(res);
1650
- }
1651
- } catch (err) {
1652
- if (isNotFoundError(err)) {
1653
- return;
1654
- }
1655
- const classified = signal?.aborted === true && !(err instanceof LlmError) ? new LlmError("xAI file delete aborted", {
1656
- kind: "aborted",
1657
- retryable: false,
1658
- provider: "xai",
1659
- cause: err
1660
- }) : classifyStoreError(err);
1661
- if (failClosed) {
1662
- 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);
1663
3279
  }
1664
- this.onDeleteError(fileId, classified);
1665
- }
3280
+ });
1666
3281
  }
1667
3282
  /**
1668
3283
  * Delete many files.
@@ -1684,36 +3299,48 @@ var XaiFileStore = class {
1684
3299
  if (typeof fileId !== "string" || fileId.trim() === "") {
1685
3300
  throw badRequest("fileId must be a non-empty string.");
1686
3301
  }
1687
- let res;
1688
- try {
1689
- res = await this.fetchImpl(
1690
- this.filesUrl(`${fileId}/content`),
1691
- this.requestInit("GET", signal !== void 0 ? { signal } : {})
1692
- );
1693
- } catch (e) {
1694
- if (signal?.aborted === true) {
1695
- throw new LlmError("xAI file content download aborted", {
1696
- kind: "aborted",
1697
- retryable: false,
1698
- provider: "xai"
1699
- });
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
+ }
1700
3320
  }
1701
- throw classifyStoreError(e);
1702
- }
1703
- if (res.status === 404) {
1704
- throw notFoundError(fileId, "getContent");
1705
- }
1706
- if (!res.ok) {
1707
3321
  try {
1708
- await throwHttpFailure(res);
3322
+ return new Uint8Array(await res.arrayBuffer());
1709
3323
  } catch (e) {
3324
+ if (bound.aborted)
3325
+ throw abortedError(bound, "xAI file content download aborted", e);
1710
3326
  throw classifyStoreError(e);
1711
3327
  }
1712
- }
1713
- const buf = await res.arrayBuffer();
1714
- return new Uint8Array(buf);
3328
+ });
1715
3329
  }
1716
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
+ }
1717
3344
 
1718
3345
  // src/provider.ts
1719
3346
  function xaiProvider(opts) {
@@ -1724,6 +3351,6 @@ function xaiProvider(opts) {
1724
3351
  };
1725
3352
  }
1726
3353
 
1727
- 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 };
1728
3355
  //# sourceMappingURL=index.js.map
1729
3356
  //# sourceMappingURL=index.js.map