@gullabs/xai 0.2.5 → 0.4.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/README.md +56 -2
- package/dist/index.cjs +471 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +153 -3
- package/dist/index.d.ts +153 -3
- package/dist/index.js +468 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -30,6 +30,10 @@ xAI has no first-party TypeScript SDK. xAI's own quickstart recommends using the
|
|
|
30
30
|
| `XaiModelRates` | Per-model rate entry type (`inputPerM`, `cachedPerM`, `outputPerM`, optional `gt200k`) |
|
|
31
31
|
| `Grok45ConfigSchema` | Strict Zod config schema for `grok-4.5` |
|
|
32
32
|
| `XaiProviderOptions` | `{ promptCacheKey? }` — typed `providerOptions.xai` extension shape |
|
|
33
|
+
| `XaiFileStore` | Files API store: upload (TTL), get, list, idempotent delete, content |
|
|
34
|
+
| `XaiFileHandle` | `{ id, filename?, bytes?, expiresAt?, … }` returned by the store |
|
|
35
|
+
| `FileDeleteOptions` | `{ failClosed?, signal? }` — opt-in fail-closed delete for durable release gates |
|
|
36
|
+
| `XAI_FILE_TTL_*` | TTL bounds (`3600`…`2592000` seconds) and `XAI_FILE_MAX_BYTES` (48 MiB) |
|
|
33
37
|
|
|
34
38
|
## Quick example
|
|
35
39
|
|
|
@@ -63,12 +67,62 @@ The default registry ships exactly one model: `grok-4.5` (500k token context win
|
|
|
63
67
|
- **No penalties/stop** — `presence_penalty`, `frequency_penalty`, and `stop` are not in the config schema at all; xAI hard-rejects these on reasoning models, so the schema never admits them (reject-don't-map).
|
|
64
68
|
- **No service tiers** — xAI has no service-tier concept for `grok-4.5`; setting `serviceTier` on the request throws `bad_request`.
|
|
65
69
|
|
|
70
|
+
## Files store (`XaiFileStore`)
|
|
71
|
+
|
|
72
|
+
Thin REST wrapper over xAI Files (`POST/GET/DELETE /v1/files`). Auth is injected — the store never reads `process.env`.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { XaiFileStore } from '@gullabs/xai'
|
|
76
|
+
|
|
77
|
+
const store = new XaiFileStore({
|
|
78
|
+
auth: { apiKey: 'YOUR_XAI_API_KEY' },
|
|
79
|
+
// Optional: onDeleteError, logger, fetch, baseUrl
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const handle = await store.upload({
|
|
83
|
+
data: pdfBytes,
|
|
84
|
+
filename: 'matter.pdf',
|
|
85
|
+
mimeType: 'application/pdf',
|
|
86
|
+
expiresAfterSeconds: 86_400, // 24h; range 3600…2592000
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// Attach on generate via core FileRefPart:
|
|
90
|
+
// { kind: 'file-ref', fileId: handle.id }
|
|
91
|
+
|
|
92
|
+
await store.delete(handle.id) // default fail-open; 404 = success
|
|
93
|
+
await store.delete(handle.id) // safe to call twice
|
|
94
|
+
|
|
95
|
+
// Durable gate (Temporal release / orphan sweep) — mark DB only after success:
|
|
96
|
+
try {
|
|
97
|
+
await store.delete(handle.id, { failClosed: true })
|
|
98
|
+
await db.markReleased(handle.id)
|
|
99
|
+
} catch (err) {
|
|
100
|
+
// leave released_at null; do not rethrow from workflow finally
|
|
101
|
+
logger.warn({ err }, 'delete failed')
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
| Behavior | Detail |
|
|
106
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
107
|
+
| TTL | `expiresAfterSeconds` validated client-side; multipart sends `expires_after` **before** `file` (xAI requirement) |
|
|
108
|
+
| Delete (default) | Fail-open: non-404 errors call `onDeleteError` and resolve; 404 is silent success |
|
|
109
|
+
| Delete (`failClosed: true`) | Non-404 failures **throw** `LlmError`; `onDeleteError` is not called. Prefer **per-id** delete + markReleased when writing durable release state — fail-closed `deleteAll` does not cancel in-flight siblings |
|
|
110
|
+
| Empty `fileId` | Always throws `bad_request` (both modes) |
|
|
111
|
+
| Storage cost | ~$0.025/GiB/day — **not** injected into `computeCost` token lanes |
|
|
112
|
+
| ZDR teams | New uploads and `file_id` attachments are blocked by xAI; errors mention Zero Data Retention when detectable |
|
|
113
|
+
| Max size | 48 MiB (conservative vs docs 48–50 MB) |
|
|
114
|
+
|
|
115
|
+
**Billing note:** attaching files on Responses implicitly enables xAI's `attachment_search` agentic tool. Expect tool-invocation fees and reasoning tokens beyond a plain completion. When xAI returns numeric counters such as `num_server_side_tools_used` / `num_sources_used`, they appear on `usage.details` under those raw names (and full payload in `usage.raw`) for host visibility — they are **not** folded into `computeXaiCost` token lanes yet (no tool Cost lane). Collections / public URL minting are out of scope for this store.
|
|
116
|
+
|
|
117
|
+
**Host tests:** `@gullabs/testing` exports `FakeXaiFileStore` (in-memory upload/get/delete with optional TTL clock and `failClosed`).
|
|
118
|
+
|
|
66
119
|
## Vision constraints
|
|
67
120
|
|
|
68
|
-
`grok-4.5` accepts image input as an `inline-media` or `file-uri` `Part`:
|
|
121
|
+
`grok-4.5` accepts image input as an `inline-media` or `file-uri` `Part`, and document attachments as a `file-ref` `Part`:
|
|
69
122
|
|
|
70
123
|
- **`inline-media`** — only `image/jpeg` and `image/png` are accepted; anything else throws `bad_request`. The decoded payload must be at most 20 MiB (xAI's documented inline-image ceiling); larger images throw `bad_request` before the request is sent.
|
|
71
124
|
- **`file-uri`** — only accepted when the URI is a public `http(s)://` URL **and** the declared `mimeType` is jpg/png. A provider-hosted URI from another provider — for example a Gemini Files API URI (`https://generativelanguage.googleapis.com/...`) — is technically `https://` but is not dereferenceable by xAI and is not portable across providers. The adapter rejects it rather than trying to map or proxy it (reject-don't-map).
|
|
125
|
+
- **`file-ref`** — maps to Responses `{ type: 'input_file', file_id }`. Upload first with `XaiFileStore`, then pass `{ kind: 'file-ref', fileId: handle.id }`. Empty ids throw `bad_request`.
|
|
72
126
|
- **Undocumented minimum size** — xAI enforces an undocumented server-side minimum image size (observed ~8px/side, ~512 total px). This adapter does **not** pre-validate pixel dimensions; a too-small image surfaces as a live `bad_request` error from the xAI API itself, classified normally by `classifyXaiError`, not rejected client-side.
|
|
73
127
|
|
|
74
128
|
## Caching
|
|
@@ -96,7 +150,7 @@ xAI's own `/v1/models` listing surfaces `grok-4.5-latest` and `grok-build-latest
|
|
|
96
150
|
|
|
97
151
|
## Explicitly deferred (not built in v1)
|
|
98
152
|
|
|
99
|
-
- Server-side agentic tools (`web_search`, `x_search`, `code_interpreter`,
|
|
153
|
+
- Server-side agentic tools as an explicit API (`web_search`, `x_search`, `code_interpreter`, collections search, remote MCP) and their per-invocation billing lanes in `computeCost`. Note: **file attachments still auto-enable `attachment_search`** on xAI's side — that implicit tool is documented above, not modeled as a first-class library tool surface.
|
|
100
154
|
- `/v1/chat/completions` (documented by xAI as legacy) and `/v1/messages` (the Anthropic-compatible migration shim) — this adapter only targets `/v1/responses`.
|
|
101
155
|
- Batch API (`grok-4.5` is not eligible at launch) and image generation models.
|
|
102
156
|
- Stateful conversations — `store` is always sent as `false`, and `previous_response_id` is not supported.
|
package/dist/index.cjs
CHANGED
|
@@ -64,8 +64,19 @@ function mapPart(p) {
|
|
|
64
64
|
`xAI only accepts public http(s) image URLs via FileUriPart; got scheme of "${p.uri}" / mimeType "${p.mimeType}".`
|
|
65
65
|
);
|
|
66
66
|
}
|
|
67
|
+
if (p.uri.includes("generativelanguage.googleapis.com") || p.uri.includes("googleapis.com/v1beta/files")) {
|
|
68
|
+
throw badXaiRequest(
|
|
69
|
+
`xAI cannot dereference Gemini/Google Files URIs via FileUriPart; upload to xAI Files and use FileRefPart (file_id) instead. Got "${p.uri}".`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
67
72
|
return { type: "input_image", image_url: p.uri };
|
|
68
73
|
}
|
|
74
|
+
case "file-ref": {
|
|
75
|
+
if (typeof p.fileId !== "string" || p.fileId.trim() === "") {
|
|
76
|
+
throw badXaiRequest("FileRefPart.fileId must be a non-empty string.");
|
|
77
|
+
}
|
|
78
|
+
return { type: "input_file", file_id: p.fileId };
|
|
79
|
+
}
|
|
69
80
|
default:
|
|
70
81
|
return core.assertNever(p);
|
|
71
82
|
}
|
|
@@ -364,6 +375,461 @@ function xaiAdapter(opts) {
|
|
|
364
375
|
}
|
|
365
376
|
};
|
|
366
377
|
}
|
|
378
|
+
var XAI_FILE_TTL_MIN_SECONDS = 3600;
|
|
379
|
+
var XAI_FILE_TTL_MAX_SECONDS = 2592e3;
|
|
380
|
+
var XAI_FILE_MAX_BYTES = 48 * 1024 * 1024;
|
|
381
|
+
var XAI_FILES_DEFAULT_BASE_URL = "https://api.x.ai/v1";
|
|
382
|
+
function isPlainRecord2(value) {
|
|
383
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
384
|
+
}
|
|
385
|
+
function badRequest(message) {
|
|
386
|
+
return new core.LlmError(message, {
|
|
387
|
+
kind: "bad_request",
|
|
388
|
+
retryable: false,
|
|
389
|
+
provider: "xai"
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
function resolveFileId(fileIdOrHandle) {
|
|
393
|
+
if (typeof fileIdOrHandle === "string") {
|
|
394
|
+
return fileIdOrHandle;
|
|
395
|
+
}
|
|
396
|
+
return fileIdOrHandle.id;
|
|
397
|
+
}
|
|
398
|
+
function unixSecondsToDate(value) {
|
|
399
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
400
|
+
return void 0;
|
|
401
|
+
}
|
|
402
|
+
return new Date(value * 1e3);
|
|
403
|
+
}
|
|
404
|
+
function makeHandle(raw) {
|
|
405
|
+
if (typeof raw.id !== "string" || raw.id.length === 0) {
|
|
406
|
+
throw new core.LlmError("File response missing required field (id)", {
|
|
407
|
+
kind: "server",
|
|
408
|
+
retryable: false,
|
|
409
|
+
provider: "xai"
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
const handle = {
|
|
413
|
+
id: raw.id,
|
|
414
|
+
raw
|
|
415
|
+
};
|
|
416
|
+
if (typeof raw.filename === "string" && raw.filename.length > 0) {
|
|
417
|
+
handle.filename = raw.filename;
|
|
418
|
+
}
|
|
419
|
+
if (typeof raw.bytes === "number" && Number.isFinite(raw.bytes)) {
|
|
420
|
+
handle.bytes = raw.bytes;
|
|
421
|
+
}
|
|
422
|
+
if (typeof raw.purpose === "string") {
|
|
423
|
+
handle.purpose = raw.purpose;
|
|
424
|
+
}
|
|
425
|
+
const createdAt = unixSecondsToDate(raw.created_at);
|
|
426
|
+
if (createdAt !== void 0) {
|
|
427
|
+
handle.createdAt = createdAt;
|
|
428
|
+
}
|
|
429
|
+
if (raw.expires_at !== null && raw.expires_at !== void 0) {
|
|
430
|
+
const expiresAt = unixSecondsToDate(raw.expires_at);
|
|
431
|
+
if (expiresAt !== void 0) {
|
|
432
|
+
handle.expiresAt = expiresAt;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return handle;
|
|
436
|
+
}
|
|
437
|
+
function byteLengthOf(data) {
|
|
438
|
+
return data instanceof Uint8Array ? data.byteLength : data.size;
|
|
439
|
+
}
|
|
440
|
+
function toBlob(data, mimeType) {
|
|
441
|
+
if (data instanceof Blob) {
|
|
442
|
+
return data;
|
|
443
|
+
}
|
|
444
|
+
const copy = Uint8Array.from(data);
|
|
445
|
+
return mimeType !== void 0 && mimeType.length > 0 ? new Blob([copy], { type: mimeType }) : new Blob([copy]);
|
|
446
|
+
}
|
|
447
|
+
var XaiFilesHttpError = class extends Error {
|
|
448
|
+
status;
|
|
449
|
+
error;
|
|
450
|
+
constructor(status, message, errorBody) {
|
|
451
|
+
super(message);
|
|
452
|
+
this.name = "XaiFilesHttpError";
|
|
453
|
+
this.status = status;
|
|
454
|
+
this.error = errorBody;
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
async function throwHttpFailure(res) {
|
|
458
|
+
const status = res.status;
|
|
459
|
+
let bodyText = "";
|
|
460
|
+
try {
|
|
461
|
+
bodyText = await res.text();
|
|
462
|
+
} catch {
|
|
463
|
+
bodyText = "";
|
|
464
|
+
}
|
|
465
|
+
let parsed = bodyText;
|
|
466
|
+
if (bodyText.length > 0) {
|
|
467
|
+
try {
|
|
468
|
+
parsed = JSON.parse(bodyText);
|
|
469
|
+
} catch {
|
|
470
|
+
parsed = bodyText;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
let message = `xAI Files API HTTP ${status}`;
|
|
474
|
+
if (typeof parsed === "string" && parsed.length > 0) {
|
|
475
|
+
message = parsed;
|
|
476
|
+
} else if (isPlainRecord2(parsed) && typeof parsed["error"] === "string") {
|
|
477
|
+
message = parsed["error"];
|
|
478
|
+
}
|
|
479
|
+
throw new XaiFilesHttpError(status, message, parsed);
|
|
480
|
+
}
|
|
481
|
+
function isNotFoundError(err) {
|
|
482
|
+
if (err instanceof core.LlmError && err.httpStatus === 404) {
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
485
|
+
if (typeof err === "object" && err !== null) {
|
|
486
|
+
const status = err.status;
|
|
487
|
+
if (status === 404) return true;
|
|
488
|
+
const httpStatus = err.httpStatus;
|
|
489
|
+
if (httpStatus === 404) return true;
|
|
490
|
+
}
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
function notFoundError(fileId, operation) {
|
|
494
|
+
return new core.LlmError(`xAI file not found during ${operation}: "${fileId}"`, {
|
|
495
|
+
kind: "bad_request",
|
|
496
|
+
retryable: false,
|
|
497
|
+
httpStatus: 404,
|
|
498
|
+
provider: "xai"
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
function maybeZdrHint(message) {
|
|
502
|
+
if (/zero\s*data\s*retention|\bzdr\b|files?\s+(api\s+)?(disabled|unavailable|not supported)/i.test(
|
|
503
|
+
message
|
|
504
|
+
)) {
|
|
505
|
+
return `${message} (xAI Zero Data Retention blocks new file uploads and file_id attachments for this team.)`;
|
|
506
|
+
}
|
|
507
|
+
return message;
|
|
508
|
+
}
|
|
509
|
+
function extractThrownMessage(raw) {
|
|
510
|
+
if (raw === null || typeof raw !== "object") return void 0;
|
|
511
|
+
const obj = raw;
|
|
512
|
+
if (typeof obj["message"] === "string" && obj["message"].length > 0) {
|
|
513
|
+
return obj["message"];
|
|
514
|
+
}
|
|
515
|
+
const err = obj["error"];
|
|
516
|
+
if (typeof err === "string" && err.length > 0) return err;
|
|
517
|
+
if (typeof err === "object" && err !== null) {
|
|
518
|
+
const nested = err;
|
|
519
|
+
if (typeof nested["error"] === "string" && nested["error"].length > 0) {
|
|
520
|
+
return nested["error"];
|
|
521
|
+
}
|
|
522
|
+
if (typeof nested["message"] === "string" && nested["message"].length > 0) {
|
|
523
|
+
return nested["message"];
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return void 0;
|
|
527
|
+
}
|
|
528
|
+
function classifyStoreError(raw) {
|
|
529
|
+
if (raw instanceof core.LlmError) {
|
|
530
|
+
return raw;
|
|
531
|
+
}
|
|
532
|
+
const classified = classifyXaiError(raw);
|
|
533
|
+
const bodyMessage = extractThrownMessage(raw);
|
|
534
|
+
const baseMessage = bodyMessage !== void 0 && (classified.message.startsWith("HTTP ") || classified.message.length === 0) ? bodyMessage : classified.message;
|
|
535
|
+
const hinted = maybeZdrHint(baseMessage);
|
|
536
|
+
if (hinted === classified.message && classified.provider === "xai" && bodyMessage === void 0) {
|
|
537
|
+
return classified;
|
|
538
|
+
}
|
|
539
|
+
return new core.LlmError(hinted, {
|
|
540
|
+
kind: classified.kind,
|
|
541
|
+
retryable: classified.retryable,
|
|
542
|
+
...classified.httpStatus !== void 0 ? { httpStatus: classified.httpStatus } : {},
|
|
543
|
+
...classified.retryAfterMs !== void 0 ? { retryAfterMs: classified.retryAfterMs } : {},
|
|
544
|
+
provider: "xai",
|
|
545
|
+
cause: classified.cause ?? raw
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
var XaiFileStore = class {
|
|
549
|
+
apiKey;
|
|
550
|
+
baseUrl;
|
|
551
|
+
fetchImpl;
|
|
552
|
+
onDeleteError;
|
|
553
|
+
logger;
|
|
554
|
+
constructor(opts) {
|
|
555
|
+
this.apiKey = requireApiKey(opts.auth);
|
|
556
|
+
this.baseUrl = (opts.baseUrl ?? XAI_FILES_DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
557
|
+
this.fetchImpl = opts.fetch ?? fetch;
|
|
558
|
+
this.logger = opts.logger;
|
|
559
|
+
this.onDeleteError = opts.onDeleteError ?? ((fileId, err) => {
|
|
560
|
+
const message = core.redactSecrets(core.classifyError(err).message);
|
|
561
|
+
if (this.logger !== void 0) {
|
|
562
|
+
this.logger.error({ fileId, error: message }, "xai.file.delete.failed");
|
|
563
|
+
} else {
|
|
564
|
+
console.error(`[XaiFileStore] delete failed for "${fileId}":`, message);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
authHeaders() {
|
|
569
|
+
const headers = new Headers();
|
|
570
|
+
headers.set("Authorization", `Bearer ${this.apiKey}`);
|
|
571
|
+
return headers;
|
|
572
|
+
}
|
|
573
|
+
filesUrl(path = "") {
|
|
574
|
+
if (path.length === 0) return `${this.baseUrl}/files`;
|
|
575
|
+
return `${this.baseUrl}/files/${path.replace(/^\//, "")}`;
|
|
576
|
+
}
|
|
577
|
+
/** Build RequestInit without writing `signal: undefined` (exactOptionalPropertyTypes). */
|
|
578
|
+
requestInit(method, opts) {
|
|
579
|
+
const init = {
|
|
580
|
+
method,
|
|
581
|
+
headers: this.authHeaders()
|
|
582
|
+
};
|
|
583
|
+
if (opts?.body !== void 0) {
|
|
584
|
+
init.body = opts.body;
|
|
585
|
+
}
|
|
586
|
+
if (opts?.signal !== void 0) {
|
|
587
|
+
init.signal = opts.signal;
|
|
588
|
+
}
|
|
589
|
+
return init;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Upload bytes to xAI Files. Returns immediately with metadata (no poll).
|
|
593
|
+
*
|
|
594
|
+
* Multipart field order is load-bearing: `expires_after` then `purpose`
|
|
595
|
+
* then `file` — reversing order yields HTTP 400 from xAI.
|
|
596
|
+
*/
|
|
597
|
+
async upload(input, signal) {
|
|
598
|
+
if (typeof input.filename !== "string" || input.filename.trim() === "") {
|
|
599
|
+
throw badRequest("XaiFileUploadInput.filename must be a non-empty string.");
|
|
600
|
+
}
|
|
601
|
+
const size = byteLengthOf(input.data);
|
|
602
|
+
if (size > XAI_FILE_MAX_BYTES) {
|
|
603
|
+
throw badRequest(
|
|
604
|
+
`xAI file uploads must be at most ${XAI_FILE_MAX_BYTES} bytes (48 MiB); got ${size}.`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
if (input.expiresAfterSeconds !== void 0) {
|
|
608
|
+
const ttl = input.expiresAfterSeconds;
|
|
609
|
+
if (typeof ttl !== "number" || !Number.isInteger(ttl) || ttl < XAI_FILE_TTL_MIN_SECONDS || ttl > XAI_FILE_TTL_MAX_SECONDS) {
|
|
610
|
+
throw badRequest(
|
|
611
|
+
`expiresAfterSeconds must be an integer in [${XAI_FILE_TTL_MIN_SECONDS}, ${XAI_FILE_TTL_MAX_SECONDS}]; got ${String(ttl)}.`
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const purpose = input.purpose ?? "assistants";
|
|
616
|
+
const form = new FormData();
|
|
617
|
+
if (input.expiresAfterSeconds !== void 0) {
|
|
618
|
+
form.append("expires_after", String(input.expiresAfterSeconds));
|
|
619
|
+
}
|
|
620
|
+
form.append("purpose", purpose);
|
|
621
|
+
form.append("file", toBlob(input.data, input.mimeType), input.filename);
|
|
622
|
+
let res;
|
|
623
|
+
try {
|
|
624
|
+
res = await this.fetchImpl(
|
|
625
|
+
this.filesUrl(),
|
|
626
|
+
this.requestInit("POST", {
|
|
627
|
+
body: form,
|
|
628
|
+
...signal !== void 0 ? { signal } : {}
|
|
629
|
+
})
|
|
630
|
+
);
|
|
631
|
+
} catch (e) {
|
|
632
|
+
if (signal?.aborted === true) {
|
|
633
|
+
throw new core.LlmError("xAI file upload aborted", {
|
|
634
|
+
kind: "aborted",
|
|
635
|
+
retryable: false,
|
|
636
|
+
provider: "xai"
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
throw classifyStoreError(e);
|
|
640
|
+
}
|
|
641
|
+
if (!res.ok) {
|
|
642
|
+
try {
|
|
643
|
+
await throwHttpFailure(res);
|
|
644
|
+
} catch (e) {
|
|
645
|
+
throw classifyStoreError(e);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
let json;
|
|
649
|
+
try {
|
|
650
|
+
json = await res.json();
|
|
651
|
+
} catch (e) {
|
|
652
|
+
throw new core.LlmError("xAI file upload returned non-JSON body", {
|
|
653
|
+
kind: "server",
|
|
654
|
+
retryable: false,
|
|
655
|
+
provider: "xai",
|
|
656
|
+
cause: e
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
return makeHandle(json);
|
|
660
|
+
}
|
|
661
|
+
async get(fileId, signal) {
|
|
662
|
+
if (typeof fileId !== "string" || fileId.trim() === "") {
|
|
663
|
+
throw badRequest("fileId must be a non-empty string.");
|
|
664
|
+
}
|
|
665
|
+
let res;
|
|
666
|
+
try {
|
|
667
|
+
res = await this.fetchImpl(
|
|
668
|
+
this.filesUrl(fileId),
|
|
669
|
+
this.requestInit("GET", signal !== void 0 ? { signal } : {})
|
|
670
|
+
);
|
|
671
|
+
} catch (e) {
|
|
672
|
+
if (signal?.aborted === true) {
|
|
673
|
+
throw new core.LlmError("xAI file get aborted", {
|
|
674
|
+
kind: "aborted",
|
|
675
|
+
retryable: false,
|
|
676
|
+
provider: "xai"
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
throw classifyStoreError(e);
|
|
680
|
+
}
|
|
681
|
+
if (res.status === 404) {
|
|
682
|
+
throw notFoundError(fileId, "get");
|
|
683
|
+
}
|
|
684
|
+
if (!res.ok) {
|
|
685
|
+
try {
|
|
686
|
+
await throwHttpFailure(res);
|
|
687
|
+
} catch (e) {
|
|
688
|
+
throw classifyStoreError(e);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const json = await res.json();
|
|
692
|
+
return makeHandle(json);
|
|
693
|
+
}
|
|
694
|
+
async list(opts = {}, signal) {
|
|
695
|
+
if (opts.limit !== void 0) {
|
|
696
|
+
if (typeof opts.limit !== "number" || !Number.isInteger(opts.limit) || opts.limit < 1 || opts.limit > 100) {
|
|
697
|
+
throw badRequest("list.limit must be an integer in [1, 100].");
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const params = new URLSearchParams();
|
|
701
|
+
if (opts.limit !== void 0) params.set("limit", String(opts.limit));
|
|
702
|
+
if (opts.order !== void 0) params.set("order", opts.order);
|
|
703
|
+
if (opts.sortBy !== void 0) params.set("sort_by", opts.sortBy);
|
|
704
|
+
if (opts.paginationToken !== void 0) {
|
|
705
|
+
params.set("pagination_token", opts.paginationToken);
|
|
706
|
+
}
|
|
707
|
+
const qs = params.toString();
|
|
708
|
+
const url = qs.length > 0 ? `${this.filesUrl()}?${qs}` : this.filesUrl();
|
|
709
|
+
let res;
|
|
710
|
+
try {
|
|
711
|
+
res = await this.fetchImpl(
|
|
712
|
+
url,
|
|
713
|
+
this.requestInit("GET", signal !== void 0 ? { signal } : {})
|
|
714
|
+
);
|
|
715
|
+
} catch (e) {
|
|
716
|
+
if (signal?.aborted === true) {
|
|
717
|
+
throw new core.LlmError("xAI file list aborted", {
|
|
718
|
+
kind: "aborted",
|
|
719
|
+
retryable: false,
|
|
720
|
+
provider: "xai"
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
throw classifyStoreError(e);
|
|
724
|
+
}
|
|
725
|
+
if (!res.ok) {
|
|
726
|
+
try {
|
|
727
|
+
await throwHttpFailure(res);
|
|
728
|
+
} catch (e) {
|
|
729
|
+
throw classifyStoreError(e);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const json = await res.json();
|
|
733
|
+
const files = Array.isArray(json.data) ? json.data.map((f) => makeHandle(f)) : [];
|
|
734
|
+
const result = { files };
|
|
735
|
+
if (typeof json.pagination_token === "string" && json.pagination_token.length > 0) {
|
|
736
|
+
result.paginationToken = json.pagination_token;
|
|
737
|
+
}
|
|
738
|
+
return result;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Delete a file. Idempotent: HTTP 404 → success.
|
|
742
|
+
*
|
|
743
|
+
* Default (`failClosed` omitted/false): non-404 errors go to `onDeleteError`
|
|
744
|
+
* and resolve (P5 fail-open). With `failClosed: true`, non-404 errors throw
|
|
745
|
+
* typed `LlmError` and `onDeleteError` is not called.
|
|
746
|
+
*
|
|
747
|
+
* Empty/blank ids always throw `bad_request` (caller fault).
|
|
748
|
+
*/
|
|
749
|
+
async delete(fileIdOrHandle, opts) {
|
|
750
|
+
const fileId = resolveFileId(fileIdOrHandle);
|
|
751
|
+
if (typeof fileId !== "string" || fileId.trim() === "") {
|
|
752
|
+
throw badRequest("fileId must be a non-empty string.");
|
|
753
|
+
}
|
|
754
|
+
const failClosed = opts?.failClosed === true;
|
|
755
|
+
const signal = opts?.signal;
|
|
756
|
+
try {
|
|
757
|
+
const res = await this.fetchImpl(
|
|
758
|
+
this.filesUrl(fileId),
|
|
759
|
+
this.requestInit("DELETE", signal !== void 0 ? { signal } : {})
|
|
760
|
+
);
|
|
761
|
+
if (res.status === 404) {
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (!res.ok) {
|
|
765
|
+
await throwHttpFailure(res);
|
|
766
|
+
}
|
|
767
|
+
} catch (err) {
|
|
768
|
+
if (isNotFoundError(err)) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const classified = signal?.aborted === true && !(err instanceof core.LlmError) ? new core.LlmError("xAI file delete aborted", {
|
|
772
|
+
kind: "aborted",
|
|
773
|
+
retryable: false,
|
|
774
|
+
provider: "xai",
|
|
775
|
+
cause: err
|
|
776
|
+
}) : classifyStoreError(err);
|
|
777
|
+
if (failClosed) {
|
|
778
|
+
throw classified;
|
|
779
|
+
}
|
|
780
|
+
this.onDeleteError(fileId, classified);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Delete many files.
|
|
785
|
+
*
|
|
786
|
+
* Fail-open (default): `Promise.allSettled` — each failure → `onDeleteError`.
|
|
787
|
+
* Fail-closed: `Promise.all` — first throw rejects; in-flight siblings are
|
|
788
|
+
* not cancelled (partial deletes may already have succeeded at the provider).
|
|
789
|
+
* Prefer per-id delete + host DB mark when gating durable release state.
|
|
790
|
+
*/
|
|
791
|
+
async deleteAll(ids, opts) {
|
|
792
|
+
if (opts?.failClosed === true) {
|
|
793
|
+
await Promise.all(ids.map((id) => this.delete(id, opts)));
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
await Promise.allSettled(ids.map((id) => this.delete(id, opts)));
|
|
797
|
+
}
|
|
798
|
+
/** Download raw file bytes. */
|
|
799
|
+
async getContent(fileId, signal) {
|
|
800
|
+
if (typeof fileId !== "string" || fileId.trim() === "") {
|
|
801
|
+
throw badRequest("fileId must be a non-empty string.");
|
|
802
|
+
}
|
|
803
|
+
let res;
|
|
804
|
+
try {
|
|
805
|
+
res = await this.fetchImpl(
|
|
806
|
+
this.filesUrl(`${fileId}/content`),
|
|
807
|
+
this.requestInit("GET", signal !== void 0 ? { signal } : {})
|
|
808
|
+
);
|
|
809
|
+
} catch (e) {
|
|
810
|
+
if (signal?.aborted === true) {
|
|
811
|
+
throw new core.LlmError("xAI file content download aborted", {
|
|
812
|
+
kind: "aborted",
|
|
813
|
+
retryable: false,
|
|
814
|
+
provider: "xai"
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
throw classifyStoreError(e);
|
|
818
|
+
}
|
|
819
|
+
if (res.status === 404) {
|
|
820
|
+
throw notFoundError(fileId, "getContent");
|
|
821
|
+
}
|
|
822
|
+
if (!res.ok) {
|
|
823
|
+
try {
|
|
824
|
+
await throwHttpFailure(res);
|
|
825
|
+
} catch (e) {
|
|
826
|
+
throw classifyStoreError(e);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const buf = await res.arrayBuffer();
|
|
830
|
+
return new Uint8Array(buf);
|
|
831
|
+
}
|
|
832
|
+
};
|
|
367
833
|
var Grok45ConfigSchema = zod.z.strictObject({
|
|
368
834
|
temperature: zod.z.number().optional().meta({
|
|
369
835
|
title: "Temperature",
|
|
@@ -530,7 +996,12 @@ function xaiProvider(opts) {
|
|
|
530
996
|
}
|
|
531
997
|
|
|
532
998
|
exports.Grok45ConfigSchema = Grok45ConfigSchema;
|
|
999
|
+
exports.XAI_FILES_DEFAULT_BASE_URL = XAI_FILES_DEFAULT_BASE_URL;
|
|
1000
|
+
exports.XAI_FILE_MAX_BYTES = XAI_FILE_MAX_BYTES;
|
|
1001
|
+
exports.XAI_FILE_TTL_MAX_SECONDS = XAI_FILE_TTL_MAX_SECONDS;
|
|
1002
|
+
exports.XAI_FILE_TTL_MIN_SECONDS = XAI_FILE_TTL_MIN_SECONDS;
|
|
533
1003
|
exports.XAI_PRICING = XAI_PRICING;
|
|
1004
|
+
exports.XaiFileStore = XaiFileStore;
|
|
534
1005
|
exports.buildXaiClient = buildXaiClient;
|
|
535
1006
|
exports.classifyXaiError = classifyXaiError;
|
|
536
1007
|
exports.computeXaiCost = computeXaiCost;
|