@cartanova/qgrid-ai-sdk 2.10.10 → 2.11.1
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.ko.md +157 -5
- package/README.md +159 -5
- package/dist/index.d.ts +176 -4
- package/dist/index.js +1647 -269
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import { Agent } from "undici";
|
|
2
3
|
//#region src/envelope-stream-parser.ts
|
|
3
|
-
const ESCAPE_MAP =
|
|
4
|
-
"\""
|
|
5
|
-
"\\"
|
|
6
|
-
"/"
|
|
7
|
-
b
|
|
8
|
-
f
|
|
9
|
-
n
|
|
10
|
-
r
|
|
11
|
-
t
|
|
12
|
-
|
|
4
|
+
const ESCAPE_MAP = /* @__PURE__ */ new Map([
|
|
5
|
+
["\"", "\""],
|
|
6
|
+
["\\", "\\"],
|
|
7
|
+
["/", "/"],
|
|
8
|
+
["b", "\b"],
|
|
9
|
+
["f", "\f"],
|
|
10
|
+
["n", "\n"],
|
|
11
|
+
["r", "\r"],
|
|
12
|
+
["t", " "]
|
|
13
|
+
]);
|
|
13
14
|
const LITERAL_CHARS = /[0-9a-zA-Z+\-.eE]/;
|
|
14
15
|
function createEnvelopeStreamParser(answerKind) {
|
|
15
16
|
let action = null;
|
|
@@ -179,7 +180,7 @@ function createEnvelopeStreamParser(answerKind) {
|
|
|
179
180
|
unicode = "";
|
|
180
181
|
return;
|
|
181
182
|
}
|
|
182
|
-
const mapped = ESCAPE_MAP
|
|
183
|
+
const mapped = ESCAPE_MAP.get(c);
|
|
183
184
|
if (mapped === void 0) {
|
|
184
185
|
discard = true;
|
|
185
186
|
return;
|
|
@@ -253,9 +254,300 @@ function createEnvelopeStreamParser(answerKind) {
|
|
|
253
254
|
} };
|
|
254
255
|
}
|
|
255
256
|
//#endregion
|
|
257
|
+
//#region src/index.types.ts
|
|
258
|
+
/**
|
|
259
|
+
* `providerOptions.xai` for Grok models, cloned field-for-field from `@ai-sdk/xai`
|
|
260
|
+
* (`xai-responses-language-model-options.ts`) so code written for `@ai-sdk/xai` keeps parsing.
|
|
261
|
+
* Parsing only proves the shape; `mapXaiProviderOptions` decides which keys qgrid honors.
|
|
262
|
+
*/
|
|
263
|
+
const qgridXaiProviderOptionsSchema = z.object({
|
|
264
|
+
/**
|
|
265
|
+
* Constrains how hard a reasoning model thinks before responding. qgrid validates the value
|
|
266
|
+
* against the model's catalog effort set; `none` is rejected because the proxy always reasons.
|
|
267
|
+
*/
|
|
268
|
+
reasoningEffort: z.enum([
|
|
269
|
+
"none",
|
|
270
|
+
"low",
|
|
271
|
+
"medium",
|
|
272
|
+
"high",
|
|
273
|
+
"xhigh"
|
|
274
|
+
]).optional(),
|
|
275
|
+
/** Maximum number of agentic tool-calling turns. Rejected by qgrid. */
|
|
276
|
+
maxTurns: z.number().int().optional(),
|
|
277
|
+
/** Whether the model may call tools in parallel. @default true */
|
|
278
|
+
parallelToolCalls: z.boolean().optional(),
|
|
279
|
+
/** Cache key used to route requests with shared prompt prefixes. */
|
|
280
|
+
promptCacheKey: z.string().optional(),
|
|
281
|
+
/** Stable identifier used to attribute policy violations to an end user. Rejected by qgrid. */
|
|
282
|
+
safetyIdentifier: z.string().optional(),
|
|
283
|
+
serviceTier: z.enum(["default", "priority"]).optional(),
|
|
284
|
+
/** Rejected by qgrid: it always sends `store: false`. */
|
|
285
|
+
store: z.boolean().optional(),
|
|
286
|
+
/** Rejected by qgrid: it replays history itself. */
|
|
287
|
+
previousResponseId: z.string().optional(),
|
|
288
|
+
/** A unique identifier for the end user, used for abuse monitoring. Rejected by qgrid. */
|
|
289
|
+
user: z.string().optional(),
|
|
290
|
+
/** Rejected by qgrid: it fixes `include` itself. */
|
|
291
|
+
include: z.array(z.enum([
|
|
292
|
+
"file_search_call.results",
|
|
293
|
+
"web_search_call.action.sources",
|
|
294
|
+
"code_interpreter_call.outputs",
|
|
295
|
+
"reasoning.encrypted_content",
|
|
296
|
+
"no_inline_citations"
|
|
297
|
+
])).nullish()
|
|
298
|
+
});
|
|
299
|
+
const imageGenerationMetadataSchema = z.looseObject({
|
|
300
|
+
route: z.enum(["codex-images", "codex-responses"]),
|
|
301
|
+
model: z.string().optional(),
|
|
302
|
+
background: z.string().optional(),
|
|
303
|
+
quality: z.string().optional(),
|
|
304
|
+
size: z.string().optional(),
|
|
305
|
+
usage: z.looseObject({
|
|
306
|
+
input_tokens: z.number(),
|
|
307
|
+
output_tokens: z.number(),
|
|
308
|
+
total_tokens: z.number(),
|
|
309
|
+
input_tokens_details: z.looseObject({
|
|
310
|
+
image_tokens: z.number(),
|
|
311
|
+
text_tokens: z.number(),
|
|
312
|
+
cached_tokens: z.number().optional()
|
|
313
|
+
}).optional(),
|
|
314
|
+
output_tokens_details: z.looseObject({
|
|
315
|
+
image_tokens: z.number(),
|
|
316
|
+
text_tokens: z.number()
|
|
317
|
+
}).optional()
|
|
318
|
+
}).optional()
|
|
319
|
+
});
|
|
320
|
+
const queryContentPartSchema = z.discriminatedUnion("type", [
|
|
321
|
+
z.object({
|
|
322
|
+
type: z.literal("text"),
|
|
323
|
+
text: z.string()
|
|
324
|
+
}),
|
|
325
|
+
z.object({
|
|
326
|
+
type: z.literal("tool-call"),
|
|
327
|
+
toolCallId: z.string(),
|
|
328
|
+
toolName: z.string(),
|
|
329
|
+
input: z.string()
|
|
330
|
+
}),
|
|
331
|
+
z.object({
|
|
332
|
+
type: z.literal("image"),
|
|
333
|
+
data: z.string(),
|
|
334
|
+
mediaType: z.enum([
|
|
335
|
+
"image/png",
|
|
336
|
+
"image/jpeg",
|
|
337
|
+
"image/webp"
|
|
338
|
+
]).optional(),
|
|
339
|
+
revisedPrompt: z.string().nullable().optional(),
|
|
340
|
+
generation: imageGenerationMetadataSchema.optional()
|
|
341
|
+
})
|
|
342
|
+
]);
|
|
343
|
+
const threadCoordSchema = z.looseObject({
|
|
344
|
+
workerId: z.number(),
|
|
345
|
+
threadId: z.string(),
|
|
346
|
+
epoch: z.number(),
|
|
347
|
+
systemHash: z.string().optional()
|
|
348
|
+
});
|
|
349
|
+
/**
|
|
350
|
+
* Response of `/api/qgrid/query` and the stream `done` event. Content variants this SDK does not
|
|
351
|
+
* know are skipped rather than rejected, so a newer server can add variants. The object stays
|
|
352
|
+
* loose because `providerMetadata` is read by a separate schema.
|
|
353
|
+
*/
|
|
354
|
+
const queryOutputSchema = z.looseObject({
|
|
355
|
+
requestLogId: z.number().optional(),
|
|
356
|
+
text: z.string(),
|
|
357
|
+
content: z.array(z.looseObject({ type: z.string() })).optional().transform((items) => items?.flatMap((item) => {
|
|
358
|
+
const part = queryContentPartSchema.safeParse(item);
|
|
359
|
+
return part.success ? [part.data] : [];
|
|
360
|
+
})),
|
|
361
|
+
finishReason: z.enum(["stop", "tool-calls"]).optional(),
|
|
362
|
+
model: z.string().optional(),
|
|
363
|
+
requestedModel: z.string().optional(),
|
|
364
|
+
modelFallbacks: z.array(z.object({
|
|
365
|
+
trigger: z.literal("refusal"),
|
|
366
|
+
fromModel: z.string(),
|
|
367
|
+
toModel: z.string(),
|
|
368
|
+
category: z.string().optional(),
|
|
369
|
+
explanation: z.string().optional()
|
|
370
|
+
})).optional(),
|
|
371
|
+
tokenName: z.string().optional(),
|
|
372
|
+
usage: z.object({
|
|
373
|
+
input_tokens: z.number(),
|
|
374
|
+
output_tokens: z.number(),
|
|
375
|
+
reasoning_tokens: z.number().optional(),
|
|
376
|
+
cache_creation_input_tokens: z.number(),
|
|
377
|
+
cache_creation_5m_input_tokens: z.number().optional(),
|
|
378
|
+
cache_creation_1h_input_tokens: z.number().optional(),
|
|
379
|
+
cache_read_input_tokens: z.number()
|
|
380
|
+
}),
|
|
381
|
+
durationMs: z.number().optional(),
|
|
382
|
+
costUsd: z.number().optional(),
|
|
383
|
+
costSource: z.enum([
|
|
384
|
+
"provider",
|
|
385
|
+
"pricing_table",
|
|
386
|
+
"mixed"
|
|
387
|
+
]).optional(),
|
|
388
|
+
runContext: z.object({
|
|
389
|
+
requestLogId: z.number().optional(),
|
|
390
|
+
threadCoord: threadCoordSchema.optional()
|
|
391
|
+
}).optional()
|
|
392
|
+
});
|
|
393
|
+
//#endregion
|
|
394
|
+
//#region src/grok-options.ts
|
|
395
|
+
/**
|
|
396
|
+
* How each documented `providerOptions.xai` key reaches the qgrid request.
|
|
397
|
+
* faithful: same meaning, same name. transformed: renamed and validated. rejected: dropped with a warning.
|
|
398
|
+
*/
|
|
399
|
+
const XAI_PROVIDER_OPTION_CLASSIFICATION = {
|
|
400
|
+
reasoningEffort: {
|
|
401
|
+
kind: "transformed",
|
|
402
|
+
target: "effort"
|
|
403
|
+
},
|
|
404
|
+
promptCacheKey: {
|
|
405
|
+
kind: "faithful",
|
|
406
|
+
target: "promptCacheKey"
|
|
407
|
+
},
|
|
408
|
+
serviceTier: {
|
|
409
|
+
kind: "rejected",
|
|
410
|
+
reason: "the Grok subscription proxy's handling of serviceTier is unverified"
|
|
411
|
+
},
|
|
412
|
+
parallelToolCalls: {
|
|
413
|
+
kind: "rejected",
|
|
414
|
+
reason: "the Grok subscription proxy's handling of parallelToolCalls is unverified"
|
|
415
|
+
},
|
|
416
|
+
store: {
|
|
417
|
+
kind: "rejected",
|
|
418
|
+
reason: "qgrid always sends store: false"
|
|
419
|
+
},
|
|
420
|
+
previousResponseId: {
|
|
421
|
+
kind: "rejected",
|
|
422
|
+
reason: "qgrid replays conversation history itself"
|
|
423
|
+
},
|
|
424
|
+
include: {
|
|
425
|
+
kind: "rejected",
|
|
426
|
+
reason: "qgrid fixes the include list itself"
|
|
427
|
+
},
|
|
428
|
+
maxTurns: {
|
|
429
|
+
kind: "rejected",
|
|
430
|
+
reason: "qgrid does not run server-side agentic turns"
|
|
431
|
+
},
|
|
432
|
+
user: {
|
|
433
|
+
kind: "rejected",
|
|
434
|
+
reason: "qgrid does not forward end-user identifiers"
|
|
435
|
+
},
|
|
436
|
+
safetyIdentifier: {
|
|
437
|
+
kind: "rejected",
|
|
438
|
+
reason: "qgrid does not forward end-user identifiers"
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
/** Catalog effort set (`/models` `reasoning_efforts`) observed at GROK_BUILD_VERSION 1.0.60. */
|
|
442
|
+
const DEFAULT_GROK_EFFORTS = [
|
|
443
|
+
"xhigh",
|
|
444
|
+
"high",
|
|
445
|
+
"medium",
|
|
446
|
+
"low"
|
|
447
|
+
];
|
|
448
|
+
/** Per-model overrides of `DEFAULT_GROK_EFFORTS`, keyed by the backend id without the `grok/` prefix. */
|
|
449
|
+
const GROK_MODEL_EFFORTS = /* @__PURE__ */ new Map([
|
|
450
|
+
["grok-4.7", DEFAULT_GROK_EFFORTS],
|
|
451
|
+
["grok-4.7-build-fast", DEFAULT_GROK_EFFORTS],
|
|
452
|
+
["grok-4.6", DEFAULT_GROK_EFFORTS],
|
|
453
|
+
["grok-4.5", DEFAULT_GROK_EFFORTS]
|
|
454
|
+
]);
|
|
455
|
+
/** Thrown when `providerOptions.xai` has an invalid shape or value type. */
|
|
456
|
+
var QgridXaiOptionsError = class extends Error {
|
|
457
|
+
issues;
|
|
458
|
+
constructor(issues) {
|
|
459
|
+
const detail = issues.map((issue) => `${issue.path || "(root)"}: ${issue.message}`).join("; ");
|
|
460
|
+
super(`qgrid: invalid providerOptions.xai: ${detail}`);
|
|
461
|
+
this.name = "QgridXaiOptionsError";
|
|
462
|
+
this.issues = issues;
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
/** Object.getPrototypeOf boxes primitives, so only plain and null-prototype objects pass. */
|
|
466
|
+
function isPlainJsonObject(value) {
|
|
467
|
+
if (value === null || Array.isArray(value)) return false;
|
|
468
|
+
const proto = Object.getPrototypeOf(value);
|
|
469
|
+
return proto === Object.prototype || proto === null;
|
|
470
|
+
}
|
|
471
|
+
function isClassifiedKey(key) {
|
|
472
|
+
return Object.hasOwn(XAI_PROVIDER_OPTION_CLASSIFICATION, key);
|
|
473
|
+
}
|
|
474
|
+
function isGrokEffort(value) {
|
|
475
|
+
return value === "low" || value === "medium" || value === "high" || value === "xhigh";
|
|
476
|
+
}
|
|
477
|
+
function grokEffortsForModel(modelId) {
|
|
478
|
+
const backendId = modelId.startsWith("grok/") ? modelId.slice(5) : modelId;
|
|
479
|
+
return GROK_MODEL_EFFORTS.get(backendId) ?? DEFAULT_GROK_EFFORTS;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Validates `providerOptions.xai` (the `@ai-sdk/xai` option shape) and maps the keys qgrid honors
|
|
483
|
+
* onto its request fields. Unsupported keys and values become warnings; malformed values throw
|
|
484
|
+
* `QgridXaiOptionsError`.
|
|
485
|
+
*/
|
|
486
|
+
function mapXaiProviderOptions(options, modelId) {
|
|
487
|
+
const result = {
|
|
488
|
+
request: {},
|
|
489
|
+
warnings: [],
|
|
490
|
+
rejected: []
|
|
491
|
+
};
|
|
492
|
+
if (options === void 0 || options === null) return result;
|
|
493
|
+
if (!isPlainJsonObject(options)) throw new QgridXaiOptionsError([{
|
|
494
|
+
path: "",
|
|
495
|
+
message: "expected an object"
|
|
496
|
+
}]);
|
|
497
|
+
const parsed = qgridXaiProviderOptionsSchema.safeParse(options);
|
|
498
|
+
if (!parsed.success) throw new QgridXaiOptionsError(parsed.error.issues.map((issue) => ({
|
|
499
|
+
path: issue.path.map(String).join("."),
|
|
500
|
+
message: issue.message
|
|
501
|
+
})));
|
|
502
|
+
const values = parsed.data;
|
|
503
|
+
for (const key of Object.keys(options)) {
|
|
504
|
+
if (!isClassifiedKey(key)) {
|
|
505
|
+
result.warnings.push({
|
|
506
|
+
key,
|
|
507
|
+
reason: "unknown providerOptions.xai key; ignored by qgrid"
|
|
508
|
+
});
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const value = values[key];
|
|
512
|
+
if (value === void 0 || value === null) continue;
|
|
513
|
+
const classification = XAI_PROVIDER_OPTION_CLASSIFICATION[key];
|
|
514
|
+
if (classification.kind === "rejected") {
|
|
515
|
+
result.warnings.push({
|
|
516
|
+
key,
|
|
517
|
+
reason: classification.reason
|
|
518
|
+
});
|
|
519
|
+
result.rejected.push(key);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const effort = values.reasoningEffort;
|
|
523
|
+
if (effort !== void 0) if (effort === "none") {
|
|
524
|
+
result.warnings.push({
|
|
525
|
+
key: "reasoningEffort",
|
|
526
|
+
reason: "reasoningEffort \"none\" is not supported; the qgrid Grok route always reasons"
|
|
527
|
+
});
|
|
528
|
+
result.rejected.push("reasoningEffort");
|
|
529
|
+
} else if (isGrokEffort(effort) && grokEffortsForModel(modelId).includes(effort)) result.request.effort = effort;
|
|
530
|
+
else {
|
|
531
|
+
result.warnings.push({
|
|
532
|
+
key: "reasoningEffort",
|
|
533
|
+
reason: `reasoningEffort "${effort}" is not supported by ${modelId}`
|
|
534
|
+
});
|
|
535
|
+
result.rejected.push("reasoningEffort");
|
|
536
|
+
}
|
|
537
|
+
if (values.promptCacheKey !== void 0) result.request.promptCacheKey = values.promptCacheKey;
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
//#endregion
|
|
256
541
|
//#region src/http-transport.ts
|
|
257
542
|
const DEFAULT_ANTHROPIC_TIMEOUT_MS = 24e4;
|
|
258
543
|
const TRANSPORT_RESPONSE_GRACE_MS = 6e4;
|
|
544
|
+
const errorChainLinkSchema = z.looseObject({
|
|
545
|
+
cause: z.unknown().optional(),
|
|
546
|
+
code: z.unknown().optional(),
|
|
547
|
+
errors: z.unknown().optional(),
|
|
548
|
+
message: z.unknown().optional()
|
|
549
|
+
});
|
|
550
|
+
const nestedErrorsSchema = z.array(z.unknown());
|
|
259
551
|
function anthropicTransportOptions(timeoutMs) {
|
|
260
552
|
const transportTimeoutMs = (Number.isFinite(timeoutMs) && timeoutMs !== void 0 && timeoutMs > 0 ? timeoutMs : DEFAULT_ANTHROPIC_TIMEOUT_MS) + TRANSPORT_RESPONSE_GRACE_MS;
|
|
261
553
|
return {
|
|
@@ -282,49 +574,51 @@ async function qgridFetch(input, init, context) {
|
|
|
282
574
|
throw toQgridTransportError(error, context);
|
|
283
575
|
}
|
|
284
576
|
}
|
|
285
|
-
function toQgridTransportError(
|
|
286
|
-
if (isAbortError(
|
|
287
|
-
const code = findErrorCode(
|
|
577
|
+
function toQgridTransportError(cause, context) {
|
|
578
|
+
if (isAbortError$1(cause)) return cause;
|
|
579
|
+
const code = findErrorCode(cause);
|
|
288
580
|
const prefix = `qgrid ${context.operation} transport failed`;
|
|
289
581
|
if (code === "UND_ERR_HEADERS_TIMEOUT") {
|
|
290
582
|
const budget = context.transportTimeoutMs ? ` after ${context.transportTimeoutMs}ms` : "";
|
|
291
|
-
return new Error(`${prefix}: response headers timed out${budget} (${code})`, { cause
|
|
583
|
+
return new Error(`${prefix}: response headers timed out${budget} (${code})`, { cause });
|
|
292
584
|
}
|
|
293
585
|
if (code === "UND_ERR_BODY_TIMEOUT") {
|
|
294
586
|
const budget = context.transportTimeoutMs ? ` after ${context.transportTimeoutMs}ms` : "";
|
|
295
|
-
return new Error(`${prefix}: response body timed out${budget} (${code})`, { cause
|
|
587
|
+
return new Error(`${prefix}: response body timed out${budget} (${code})`, { cause });
|
|
296
588
|
}
|
|
297
|
-
if (code === "ECONNREFUSED") return new Error(`${prefix}: connection refused by ${serverOrigin(context.serverUrl)} (${code})`, { cause
|
|
298
|
-
const detail = findDeepestMessage(
|
|
299
|
-
return new Error(`${prefix}: ${detail}${code ? ` (${code})` : ""}`, { cause
|
|
589
|
+
if (code === "ECONNREFUSED") return new Error(`${prefix}: connection refused by ${serverOrigin(context.serverUrl)} (${code})`, { cause });
|
|
590
|
+
const detail = findDeepestMessage(cause) ?? "unknown network error";
|
|
591
|
+
return new Error(`${prefix}: ${detail}${code ? ` (${code})` : ""}`, { cause });
|
|
300
592
|
}
|
|
301
|
-
function isAbortError(error) {
|
|
593
|
+
function isAbortError$1(error) {
|
|
302
594
|
return error instanceof Error && error.name === "AbortError";
|
|
303
595
|
}
|
|
304
|
-
function findErrorCode(
|
|
305
|
-
|
|
306
|
-
seen.
|
|
307
|
-
|
|
308
|
-
const
|
|
596
|
+
function findErrorCode(cause, seen = /* @__PURE__ */ new Set()) {
|
|
597
|
+
const link = errorChainLinkSchema.safeParse(cause);
|
|
598
|
+
if (!link.success || seen.has(cause)) return void 0;
|
|
599
|
+
seen.add(cause);
|
|
600
|
+
const code = z.string().safeParse(link.data.code);
|
|
601
|
+
if (code.success) return code.data;
|
|
602
|
+
const causeCode = findErrorCode(link.data.cause, seen);
|
|
309
603
|
if (causeCode) return causeCode;
|
|
310
|
-
|
|
311
|
-
|
|
604
|
+
const nested = nestedErrorsSchema.safeParse(link.data.errors);
|
|
605
|
+
if (nested.success) for (const nestedError of nested.data) {
|
|
606
|
+
const nestedCode = findErrorCode(nestedError, seen);
|
|
312
607
|
if (nestedCode) return nestedCode;
|
|
313
608
|
}
|
|
314
609
|
}
|
|
315
|
-
function findDeepestMessage(
|
|
316
|
-
|
|
317
|
-
seen.
|
|
318
|
-
|
|
610
|
+
function findDeepestMessage(cause, seen = /* @__PURE__ */ new Set()) {
|
|
611
|
+
const link = errorChainLinkSchema.safeParse(cause);
|
|
612
|
+
if (!link.success || seen.has(cause)) return void 0;
|
|
613
|
+
seen.add(cause);
|
|
614
|
+
const causeMessage = findDeepestMessage(link.data.cause, seen);
|
|
319
615
|
if (causeMessage) return causeMessage;
|
|
320
|
-
|
|
321
|
-
|
|
616
|
+
const nested = nestedErrorsSchema.safeParse(link.data.errors);
|
|
617
|
+
if (nested.success) for (const nestedError of nested.data) {
|
|
618
|
+
const nestedMessage = findDeepestMessage(nestedError, seen);
|
|
322
619
|
if (nestedMessage) return nestedMessage;
|
|
323
620
|
}
|
|
324
|
-
return
|
|
325
|
-
}
|
|
326
|
-
function isErrorLike(value) {
|
|
327
|
-
return typeof value === "object" && value !== null;
|
|
621
|
+
return z.string().min(1).safeParse(link.data.message).data;
|
|
328
622
|
}
|
|
329
623
|
function serverOrigin(serverUrl) {
|
|
330
624
|
try {
|
|
@@ -342,33 +636,52 @@ const TOP_LEVEL_SCHEMA_WARNING = "qgrid server-enforced structured output requir
|
|
|
342
636
|
const PENDING_TOOL_RESULTS_WARNING = "[qgrid] tool results did not match any pending qgrid run; sending request without runContext";
|
|
343
637
|
//#endregion
|
|
344
638
|
//#region src/utils.ts
|
|
639
|
+
const createRunOutputSchema = z.object({ requestLogId: z.number() });
|
|
640
|
+
const appendStepOutputSchema = z.object({ stepId: z.number() });
|
|
641
|
+
const finishRunOutputSchema = z.object({ ok: z.boolean() });
|
|
345
642
|
async function createRun(serverUrl, body) {
|
|
346
|
-
|
|
643
|
+
const res = await fetch(`${serverUrl}/api/qgrid/createRun`, {
|
|
347
644
|
method: "POST",
|
|
348
645
|
headers: { "Content-Type": "application/json" },
|
|
349
646
|
body: JSON.stringify({ input: body })
|
|
350
|
-
})
|
|
647
|
+
});
|
|
648
|
+
return createRunOutputSchema.parse(await res.json());
|
|
351
649
|
}
|
|
352
650
|
async function appendStep(serverUrl, body) {
|
|
353
|
-
|
|
651
|
+
const res = await fetch(`${serverUrl}/api/qgrid/appendStep`, {
|
|
354
652
|
method: "POST",
|
|
355
653
|
headers: { "Content-Type": "application/json" },
|
|
356
654
|
body: JSON.stringify({ input: body })
|
|
357
|
-
})
|
|
655
|
+
});
|
|
656
|
+
return appendStepOutputSchema.parse(await res.json());
|
|
358
657
|
}
|
|
359
658
|
async function finishRun(serverUrl, body) {
|
|
360
|
-
|
|
659
|
+
const res = await fetch(`${serverUrl}/api/qgrid/finishRun`, {
|
|
361
660
|
method: "POST",
|
|
362
661
|
headers: { "Content-Type": "application/json" },
|
|
363
662
|
body: JSON.stringify({ input: body })
|
|
364
|
-
})
|
|
663
|
+
});
|
|
664
|
+
return finishRunOutputSchema.parse(await res.json());
|
|
365
665
|
}
|
|
666
|
+
/**
|
|
667
|
+
* Decodes an AI SDK payload (tool input, tool output, tool error) into request-log text. JSON
|
|
668
|
+
* cannot encode every JavaScript value (cycles, BigInt, bare undefined); those fall back to String().
|
|
669
|
+
*/
|
|
670
|
+
const loggedPayloadText = z.unknown().transform((payload) => {
|
|
671
|
+
try {
|
|
672
|
+
const json = JSON.stringify(payload);
|
|
673
|
+
return json === void 0 ? String(payload) : json;
|
|
674
|
+
} catch {
|
|
675
|
+
return String(payload);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
/** Like loggedPayloadText, but keeps strings verbatim (tool arguments that are already JSON text). */
|
|
679
|
+
const toolPayloadText = z.union([z.string(), loggedPayloadText]);
|
|
366
680
|
function toQgridTool(tool) {
|
|
367
|
-
const source = tool;
|
|
368
681
|
return {
|
|
369
682
|
name: tool.name,
|
|
370
683
|
description: tool.description,
|
|
371
|
-
inputSchema:
|
|
684
|
+
inputSchema: tool.inputSchema
|
|
372
685
|
};
|
|
373
686
|
}
|
|
374
687
|
function extractToolResultsFromHistory(messages) {
|
|
@@ -377,13 +690,13 @@ function extractToolResultsFromHistory(messages) {
|
|
|
377
690
|
for (const msg of messages) if (msg.role === "assistant") {
|
|
378
691
|
for (const part of msg.content) if ("type" in part && part.type === "tool-call") calls.set(part.toolCallId, {
|
|
379
692
|
toolName: part.toolName,
|
|
380
|
-
args:
|
|
693
|
+
args: toolPayloadText.parse(part.input)
|
|
381
694
|
});
|
|
382
695
|
} else if (msg.role === "tool") {
|
|
383
696
|
for (const part of msg.content) if ("type" in part && part.type === "tool-result") {
|
|
384
697
|
const id = "toolCallId" in part ? part.toolCallId : "";
|
|
385
698
|
const output = part.output;
|
|
386
|
-
const text = "value" in output ?
|
|
699
|
+
const text = "value" in output ? toolPayloadText.parse(output.value) : JSON.stringify(output);
|
|
387
700
|
results.set(id, text);
|
|
388
701
|
}
|
|
389
702
|
}
|
|
@@ -446,7 +759,7 @@ function extractPromptAndHistory(messages, options = {}) {
|
|
|
446
759
|
droppedImageCount += extracted.droppedImageCount;
|
|
447
760
|
return extracted;
|
|
448
761
|
};
|
|
449
|
-
for (const msg of messages) if (msg.role === "system") system =
|
|
762
|
+
for (const msg of messages) if (msg.role === "system") system = msg.content;
|
|
450
763
|
else nonSystem.push(msg);
|
|
451
764
|
if (nonSystem.length === 0) return {
|
|
452
765
|
prompt: "",
|
|
@@ -479,7 +792,7 @@ function extractPromptAndHistory(messages, options = {}) {
|
|
|
479
792
|
}]
|
|
480
793
|
});
|
|
481
794
|
} else if (msg.role === "assistant") {
|
|
482
|
-
for (const part of msg.content) if ("text" in part
|
|
795
|
+
for (const part of msg.content) if ("text" in part) history.push({
|
|
483
796
|
type: "message",
|
|
484
797
|
role: "assistant",
|
|
485
798
|
content: [{
|
|
@@ -490,14 +803,14 @@ function extractPromptAndHistory(messages, options = {}) {
|
|
|
490
803
|
else if ("toolName" in part && part.type === "tool-call") history.push({
|
|
491
804
|
type: "function_call",
|
|
492
805
|
name: part.toolName,
|
|
493
|
-
arguments:
|
|
806
|
+
arguments: toolPayloadText.parse(part.input),
|
|
494
807
|
call_id: part.toolCallId
|
|
495
808
|
});
|
|
496
809
|
else if (isImagePart(part)) droppedImageCount++;
|
|
497
810
|
} else if (msg.role === "tool") {
|
|
498
811
|
for (const part of msg.content) if ("toolName" in part && part.type === "tool-result") {
|
|
499
812
|
const output = part.output;
|
|
500
|
-
const text = "value" in output ?
|
|
813
|
+
const text = "value" in output ? toolPayloadText.parse(output.value) : JSON.stringify(output);
|
|
501
814
|
history.push({
|
|
502
815
|
type: "function_call_output",
|
|
503
816
|
call_id: ("toolCallId" in part ? part.toolCallId : "") ?? "",
|
|
@@ -506,30 +819,22 @@ function extractPromptAndHistory(messages, options = {}) {
|
|
|
506
819
|
}
|
|
507
820
|
}
|
|
508
821
|
}
|
|
509
|
-
|
|
822
|
+
const result = {
|
|
510
823
|
prompt,
|
|
511
824
|
system,
|
|
512
825
|
history,
|
|
513
826
|
imageUrls,
|
|
514
|
-
droppedImageCount
|
|
515
|
-
...input ? { input } : {}
|
|
827
|
+
droppedImageCount
|
|
516
828
|
};
|
|
829
|
+
if (input) result.input = input;
|
|
830
|
+
return result;
|
|
517
831
|
}
|
|
518
832
|
function extractPromptParts(content, includeImages) {
|
|
519
|
-
if (typeof content === "string") return {
|
|
520
|
-
text: content,
|
|
521
|
-
parts: [{
|
|
522
|
-
type: "input_text",
|
|
523
|
-
text: content
|
|
524
|
-
}],
|
|
525
|
-
imageUrls: [],
|
|
526
|
-
droppedImageCount: 0
|
|
527
|
-
};
|
|
528
833
|
const parts = [];
|
|
529
834
|
const texts = [];
|
|
530
835
|
const imageUrls = [];
|
|
531
836
|
let droppedImageCount = 0;
|
|
532
|
-
for (const part of content) if ("text" in part
|
|
837
|
+
for (const part of content) if ("text" in part) {
|
|
533
838
|
parts.push({
|
|
534
839
|
type: "input_text",
|
|
535
840
|
text: part.text
|
|
@@ -567,18 +872,13 @@ function inputPartsFromPromptParts(parts) {
|
|
|
567
872
|
});
|
|
568
873
|
}
|
|
569
874
|
function isImagePart(part) {
|
|
570
|
-
|
|
571
|
-
const record = part;
|
|
572
|
-
return record.type === "image" || record.type === "file" && typeof record.mediaType === "string" && record.mediaType.toLowerCase().startsWith("image/");
|
|
875
|
+
return part.type === "file" && part.mediaType.toLowerCase().startsWith("image/");
|
|
573
876
|
}
|
|
574
877
|
function imageUrl(part) {
|
|
575
|
-
const
|
|
576
|
-
const
|
|
577
|
-
const data = record.data ?? record.image;
|
|
878
|
+
const mediaType = part.mediaType === "image/*" ? "image/png" : part.mediaType;
|
|
879
|
+
const { data } = part;
|
|
578
880
|
if (data instanceof URL) return /^https?:$/i.test(data.protocol) ? data.toString() : void 0;
|
|
579
881
|
if (data instanceof Uint8Array) return `data:${mediaType};base64,${bytesToBase64(data)}`;
|
|
580
|
-
if (data instanceof ArrayBuffer) return `data:${mediaType};base64,${bytesToBase64(new Uint8Array(data))}`;
|
|
581
|
-
if (typeof data !== "string") return void 0;
|
|
582
882
|
if (/^(https?:|data:)/i.test(data)) return data;
|
|
583
883
|
return /^[a-z][a-z0-9+.-]*:/i.test(data) ? void 0 : `data:${mediaType};base64,${data}`;
|
|
584
884
|
}
|
|
@@ -600,73 +900,63 @@ function assertImageInputFitsJsonTransport(imageUrls) {
|
|
|
600
900
|
}
|
|
601
901
|
if (totalDataUrlChars > MAX_IMAGE_INPUT_DATA_URL_CHARS) throw new Error(`qgrid: image inputs are too large for JSON transport (${totalDataUrlChars} total chars). Compress or resize them before passing them to qgrid, for example as WebP/JPEG under ${MAX_IMAGE_INPUT_DATA_URL_CHARS_LABEL} total base64 data-url chars.`);
|
|
602
902
|
}
|
|
603
|
-
function safeStringify(value) {
|
|
604
|
-
try {
|
|
605
|
-
const json = JSON.stringify(value);
|
|
606
|
-
return json === void 0 ? String(value) : json;
|
|
607
|
-
} catch {
|
|
608
|
-
return String(value);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
function getRecord(value) {
|
|
612
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
613
|
-
}
|
|
614
903
|
function extractTextContent(content) {
|
|
615
|
-
if (
|
|
904
|
+
if (!Array.isArray(content)) return content;
|
|
616
905
|
const parts = [];
|
|
617
|
-
if (
|
|
618
|
-
for (const part of content) if (part && typeof part === "object" && "type" in part) {
|
|
619
|
-
if (part.type === "text" && "text" in part && typeof part.text === "string") parts.push(part.text);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
906
|
+
for (const part of content) if (part.type === "text") parts.push(part.text);
|
|
622
907
|
return parts.join("\n");
|
|
623
908
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
909
|
+
const primitiveErrorSchema = z.union([
|
|
910
|
+
z.string(),
|
|
911
|
+
z.number(),
|
|
912
|
+
z.nan(),
|
|
913
|
+
z.boolean(),
|
|
914
|
+
z.bigint()
|
|
915
|
+
]);
|
|
916
|
+
/** Formats an error cause (thrown value, abort reason, AI SDK error field) for a request log. */
|
|
917
|
+
function getErrorMessage(cause) {
|
|
918
|
+
if (cause instanceof Error) return String(cause);
|
|
919
|
+
if (cause === void 0 || cause === null) return "unknown error";
|
|
920
|
+
const primitive = primitiveErrorSchema.safeParse(cause);
|
|
921
|
+
if (primitive.success) return String(primitive.data);
|
|
628
922
|
try {
|
|
629
|
-
return JSON.stringify(
|
|
923
|
+
return JSON.stringify(cause) ?? "unknown error";
|
|
630
924
|
} catch {
|
|
631
925
|
return "unknown error";
|
|
632
926
|
}
|
|
633
927
|
}
|
|
634
928
|
function extractUserPrompt(prompt, messages) {
|
|
635
|
-
if (
|
|
636
|
-
const messageList =
|
|
929
|
+
if (prompt !== void 0 && !Array.isArray(prompt)) return prompt;
|
|
930
|
+
const messageList = messages ?? prompt ?? [];
|
|
637
931
|
for (let i = messageList.length - 1; i >= 0; i--) {
|
|
638
|
-
const msg =
|
|
932
|
+
const msg = messageList[i];
|
|
639
933
|
if (msg?.role === "user") return extractTextContent(msg.content);
|
|
640
934
|
}
|
|
641
935
|
return "";
|
|
642
936
|
}
|
|
643
937
|
function extractSystemPrompt(system) {
|
|
644
|
-
if (
|
|
645
|
-
|
|
646
|
-
const content = system.content;
|
|
647
|
-
if (typeof content === "string") return content;
|
|
648
|
-
}
|
|
938
|
+
if (system === void 0 || Array.isArray(system)) return void 0;
|
|
939
|
+
return system instanceof Object ? system.content : system;
|
|
649
940
|
}
|
|
650
941
|
function serializeHistory(messages) {
|
|
651
|
-
if (!
|
|
652
|
-
const sliced =
|
|
942
|
+
if (!messages || messages.length === 0) return void 0;
|
|
943
|
+
const sliced = messages.at(-1)?.role === "user" ? messages.slice(0, -1) : messages;
|
|
653
944
|
const history = [];
|
|
654
945
|
for (const msg of sliced) {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
const text = extractTextContent(record.content);
|
|
946
|
+
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
947
|
+
const text = extractTextContent(msg.content);
|
|
658
948
|
if (text.length === 0) continue;
|
|
659
949
|
history.push({
|
|
660
950
|
type: "message",
|
|
661
|
-
role:
|
|
951
|
+
role: msg.role,
|
|
662
952
|
content: [{
|
|
663
|
-
type:
|
|
953
|
+
type: msg.role === "user" ? "input_text" : "output_text",
|
|
664
954
|
text
|
|
665
955
|
}]
|
|
666
956
|
});
|
|
667
957
|
}
|
|
668
958
|
if (history.length === 0) return void 0;
|
|
669
|
-
return
|
|
959
|
+
return JSON.stringify(history);
|
|
670
960
|
}
|
|
671
961
|
//#endregion
|
|
672
962
|
//#region src/image-model.ts
|
|
@@ -680,10 +970,198 @@ function imageInputUrl(file) {
|
|
|
680
970
|
"image/jpeg",
|
|
681
971
|
"image/webp"
|
|
682
972
|
].includes(file.mediaType)) throw new Error("qgrid.image: reference images must be PNG, JPEG, or WebP");
|
|
683
|
-
const data =
|
|
973
|
+
const data = file.data instanceof Uint8Array ? bytesToBase64(file.data) : file.data;
|
|
684
974
|
return `data:${file.mediaType};base64,${data}`;
|
|
685
975
|
}
|
|
976
|
+
/** Server-side Fastify body limit (`sonamu.config.ts` `bodyLimit`). Larger bodies get a 413. */
|
|
977
|
+
const QGRID_MAX_REQUEST_BODY_BYTES = 10 * 1024 * 1024;
|
|
978
|
+
function resolveMediaConfig(config) {
|
|
979
|
+
return {
|
|
980
|
+
serverUrl: config?.serverUrl ?? process.env.QGRID_URL ?? "http://localhost:44900",
|
|
981
|
+
projectName: config?.projectName ?? process.env.QGRID_PROJECT_NAME
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
const GROK_MODEL_ID = /^grok\/[A-Za-z0-9._-]+$/;
|
|
985
|
+
function assertGrokModelId(modelId, label) {
|
|
986
|
+
if (!GROK_MODEL_ID.test(modelId)) throw new Error(`${label}: unsupported model ${modelId}; expected grok/<id>`);
|
|
987
|
+
}
|
|
988
|
+
function formatMiB(bytes) {
|
|
989
|
+
return `${(bytes / 1024 / 1024).toFixed(2)} MiB`;
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Server `MEDIA_MAX_INLINE_BYTES` (qgrid.types.ts): decoded inline audio, each inline image, and all
|
|
993
|
+
* inline images of one request together. Derived from the 10 MiB body limit: base64 grows bytes by
|
|
994
|
+
* 4/3, so 7 MiB decoded is about 9.3 MiB encoded, leaving room for the rest of the JSON body.
|
|
995
|
+
*/
|
|
996
|
+
const QGRID_MAX_INLINE_MEDIA_BYTES = 7 * 1024 * 1024;
|
|
997
|
+
function base64DecodedBytes(data) {
|
|
998
|
+
const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
|
|
999
|
+
return Math.floor(data.length * 3 / 4) - padding;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Rejects inline media above the server's decoded-size cap before transport. Accepts base64
|
|
1003
|
+
* strings and data URLs; https URLs cost nothing because xAI fetches them.
|
|
1004
|
+
*/
|
|
1005
|
+
function assertInlineMediaFits(items, label) {
|
|
1006
|
+
let total = 0;
|
|
1007
|
+
for (const item of items) {
|
|
1008
|
+
if (item === void 0 || /^https:\/\//i.test(item)) continue;
|
|
1009
|
+
const comma = item.startsWith("data:") ? item.indexOf(",") : -1;
|
|
1010
|
+
total += base64DecodedBytes(comma >= 0 ? item.slice(comma + 1) : item);
|
|
1011
|
+
}
|
|
1012
|
+
if (total > 7340032) throw new Error(`${label}: inline media is ${formatMiB(total)}, above the qgrid server limit of ${formatMiB(QGRID_MAX_INLINE_MEDIA_BYTES)} per request; compress it or pass an https URL where supported.`);
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Serializes a Sonamu `params` body and rejects it before transport when it would exceed the
|
|
1016
|
+
* server body limit, so callers get a clear error instead of an opaque 413.
|
|
1017
|
+
*/
|
|
1018
|
+
function serializeMediaBody(params, label) {
|
|
1019
|
+
const body = JSON.stringify({ params });
|
|
1020
|
+
const bytes = new TextEncoder().encode(body).byteLength;
|
|
1021
|
+
if (bytes > 10485760) throw new Error(`${label}: request body is ${formatMiB(bytes)}, above the qgrid server limit of ${formatMiB(QGRID_MAX_REQUEST_BODY_BYTES)}; compress inline media or pass an https URL where supported.`);
|
|
1022
|
+
return body;
|
|
1023
|
+
}
|
|
1024
|
+
const ERROR_BODY_PREVIEW_CHARS = 2e3;
|
|
1025
|
+
function isAbortError(cause) {
|
|
1026
|
+
return cause instanceof Error && (cause.name === "AbortError" || cause.name === "TimeoutError");
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Rethrows `cause` when it is an abort. When `signal` fired, its own reason is thrown so callers
|
|
1030
|
+
* that compare reasons (the video poll deadline) can tell which abort it was.
|
|
1031
|
+
*/
|
|
1032
|
+
function rethrowAbort(cause, signal) {
|
|
1033
|
+
if (signal?.aborted) throw signal.reason ?? cause;
|
|
1034
|
+
if (isAbortError(cause)) throw cause;
|
|
1035
|
+
}
|
|
1036
|
+
/** Non-2xx response from a qgrid media endpoint. */
|
|
1037
|
+
var QgridMediaHttpError = class extends Error {
|
|
1038
|
+
status;
|
|
1039
|
+
constructor(message, status) {
|
|
1040
|
+
super(message);
|
|
1041
|
+
this.name = "QgridMediaHttpError";
|
|
1042
|
+
this.status = status;
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
/** POSTs one Sonamu media endpoint and validates the JSON response with `schema`. */
|
|
1046
|
+
async function postMediaEndpoint(args) {
|
|
1047
|
+
const headers = new Headers();
|
|
1048
|
+
for (const [key, value] of Object.entries(args.headers ?? {})) if (value !== void 0) headers.set(key, value);
|
|
1049
|
+
headers.set("Content-Type", "application/json");
|
|
1050
|
+
const init = {
|
|
1051
|
+
method: "POST",
|
|
1052
|
+
headers,
|
|
1053
|
+
body: args.body
|
|
1054
|
+
};
|
|
1055
|
+
if (args.signal) init.signal = args.signal;
|
|
1056
|
+
let response;
|
|
1057
|
+
try {
|
|
1058
|
+
response = await fetch(`${args.serverUrl}/api/qgrid/${args.endpoint}`, init);
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
rethrowAbort(error, args.signal);
|
|
1061
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1062
|
+
throw new Error(`${args.label}: ${args.endpoint} transport failed: ${detail}`, { cause: error });
|
|
1063
|
+
}
|
|
1064
|
+
if (!response.ok) {
|
|
1065
|
+
const text = await response.text().catch((cause) => {
|
|
1066
|
+
rethrowAbort(cause, args.signal);
|
|
1067
|
+
return "";
|
|
1068
|
+
});
|
|
1069
|
+
if (response.status === 413) throw new QgridMediaHttpError(`${args.label}: the qgrid server rejected the request as too large (413); inline media is limited to ${formatMiB(QGRID_MAX_INLINE_MEDIA_BYTES)} decoded per request. ${text.slice(0, ERROR_BODY_PREVIEW_CHARS)}`, 413);
|
|
1070
|
+
throw new QgridMediaHttpError(`qgrid ${args.endpoint} ${response.status}: ${text.slice(0, ERROR_BODY_PREVIEW_CHARS)}`, response.status);
|
|
1071
|
+
}
|
|
1072
|
+
const json = await response.json().catch((cause) => {
|
|
1073
|
+
rethrowAbort(cause, args.signal);
|
|
1074
|
+
});
|
|
1075
|
+
const parsed = args.schema.safeParse(json);
|
|
1076
|
+
if (!parsed.success) throw new Error(`${args.label}: unexpected ${args.endpoint} response; update the qgrid server and SDK together (${formatIssues(parsed.error)})`);
|
|
1077
|
+
return {
|
|
1078
|
+
data: parsed.data,
|
|
1079
|
+
headers: Object.fromEntries(response.headers)
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
function formatIssues(error) {
|
|
1083
|
+
return error.issues.map((issue) => `${issue.path.map(String).join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
1084
|
+
}
|
|
1085
|
+
const grokQgridOptionsSchema = z.object({
|
|
1086
|
+
tokenName: z.string().refine((value) => /^grok\/.+/.test(value.trim()), { message: "tokenName must be a non-empty Grok token name with the grok/ prefix" }).optional(),
|
|
1087
|
+
logger: z.boolean({ message: "logger must be a boolean" }).optional()
|
|
1088
|
+
});
|
|
1089
|
+
/** Any non-null, non-array object; field validation happens in the option schemas. */
|
|
1090
|
+
const providerOptionsObjectSchema = z.looseObject({});
|
|
1091
|
+
function parseGrokQgridOptions(raw, label) {
|
|
1092
|
+
if (raw === void 0) return {};
|
|
1093
|
+
if (!providerOptionsObjectSchema.safeParse(raw).success) throw new Error(`${label}: providerOptions.qgrid must be an object`);
|
|
1094
|
+
const parsed = grokQgridOptionsSchema.safeParse(raw);
|
|
1095
|
+
if (!parsed.success) throw new Error(`${label}: ${parsed.error.issues.map((issue) => issue.message).join("; ")}`);
|
|
1096
|
+
const request = {};
|
|
1097
|
+
if (parsed.data.tokenName !== void 0) request.tokenName = parsed.data.tokenName;
|
|
1098
|
+
if (parsed.data.logger === false) request.logger = false;
|
|
1099
|
+
return request;
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Validates `providerOptions.xai` against `schema` and warns about every set key outside
|
|
1103
|
+
* `honored`. Malformed values throw; documented-but-unsupported keys become warnings.
|
|
1104
|
+
*/
|
|
1105
|
+
function parseXaiMediaOptions(raw, schema, honored, label) {
|
|
1106
|
+
if (raw === void 0) return {
|
|
1107
|
+
options: void 0,
|
|
1108
|
+
warnings: []
|
|
1109
|
+
};
|
|
1110
|
+
const record = providerOptionsObjectSchema.safeParse(raw);
|
|
1111
|
+
if (!record.success) throw new Error(`${label}: providerOptions.xai must be an object`);
|
|
1112
|
+
const parsed = schema.safeParse(raw);
|
|
1113
|
+
if (!parsed.success) throw new Error(`${label}: invalid providerOptions.xai: ${formatIssues(parsed.error)}`);
|
|
1114
|
+
const warnings = [];
|
|
1115
|
+
for (const [key, value] of Object.entries(record.data)) {
|
|
1116
|
+
if (value === void 0 || value === null || honored.has(key)) continue;
|
|
1117
|
+
warnings.push({
|
|
1118
|
+
type: "unsupported",
|
|
1119
|
+
feature: `providerOptions.xai.${key}`,
|
|
1120
|
+
details: `qgrid does not support providerOptions.xai.${key} for this model; it was ignored.`
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
return {
|
|
1124
|
+
options: parsed.data,
|
|
1125
|
+
warnings
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
const IMAGE_REF_MEDIA_TYPES = [
|
|
1129
|
+
"image/png",
|
|
1130
|
+
"image/jpeg",
|
|
1131
|
+
"image/webp"
|
|
1132
|
+
];
|
|
1133
|
+
/** An AI SDK file as the server's image reference: an https URL or a base64 image data URL. */
|
|
1134
|
+
function toImageRef(file, label) {
|
|
1135
|
+
if (file.type === "url") return toImageRefUrl(file.url, label);
|
|
1136
|
+
const mediaType = file.mediaType.toLowerCase();
|
|
1137
|
+
if (!IMAGE_REF_MEDIA_TYPES.includes(mediaType)) throw new Error(`${label}: reference images must be PNG, JPEG, or WebP`);
|
|
1138
|
+
return `data:${mediaType};base64,${file.data instanceof Uint8Array ? bytesToBase64(file.data) : file.data}`;
|
|
1139
|
+
}
|
|
1140
|
+
/** Accepts an https URL or a base64 PNG/JPEG/WebP data URL string. */
|
|
1141
|
+
function toImageRefUrl(url, label) {
|
|
1142
|
+
if (/^https:\/\//i.test(url)) return url;
|
|
1143
|
+
if (/^data:image\/(png|jpeg|webp);base64,/i.test(url)) return url;
|
|
1144
|
+
throw new Error(`${label}: image references must be https URLs or PNG/JPEG/WebP data URLs`);
|
|
1145
|
+
}
|
|
1146
|
+
const OPENAI_IMAGE_TOKEN_NAME_MESSAGE = "tokenName must be a non-empty OpenAI token name with the openai/ prefix";
|
|
1147
|
+
/** `providerOptions.qgrid` for `qgrid.image("openai/gpt-image-2")`; issues keep this field order. */
|
|
1148
|
+
const openAIImageQgridOptionsSchema = z.object({
|
|
1149
|
+
tokenName: z.string({ message: OPENAI_IMAGE_TOKEN_NAME_MESSAGE }).refine((value) => /^openai\/.+/.test(value.trim()), { message: OPENAI_IMAGE_TOKEN_NAME_MESSAGE }).optional(),
|
|
1150
|
+
logger: z.boolean({ message: "logger must be a boolean" }).optional(),
|
|
1151
|
+
outputFormat: z.literal("png", { message: "the subscription Images endpoint supports PNG output only" }).optional(),
|
|
1152
|
+
quality: z.enum([
|
|
1153
|
+
"low",
|
|
1154
|
+
"medium",
|
|
1155
|
+
"high"
|
|
1156
|
+
], { message: "quality must be low, medium, or high" }).optional(),
|
|
1157
|
+
background: z.enum([
|
|
1158
|
+
"auto",
|
|
1159
|
+
"opaque",
|
|
1160
|
+
"transparent"
|
|
1161
|
+
], { message: "background must be auto, opaque, or transparent" }).optional()
|
|
1162
|
+
});
|
|
686
1163
|
function createQgridImage(modelId, config) {
|
|
1164
|
+
if (modelId.startsWith("grok/")) return createGrokImage(modelId, config);
|
|
687
1165
|
if (modelId !== "openai/gpt-image-2") throw new Error(`qgrid.image: unsupported image model ${modelId}`);
|
|
688
1166
|
const serverUrl = config?.serverUrl ?? process.env.QGRID_URL ?? "http://localhost:44900";
|
|
689
1167
|
const projectName = config?.projectName ?? process.env.QGRID_PROJECT_NAME;
|
|
@@ -697,20 +1175,9 @@ function createQgridImage(modelId, config) {
|
|
|
697
1175
|
if (options.n !== 1) throw new Error("qgrid.image: only one image per call is supported; omit maxImagesPerCall so AI SDK can batch n images");
|
|
698
1176
|
if (!options.prompt?.trim()) throw new Error("qgrid.image: a non-empty text prompt is required");
|
|
699
1177
|
if ((options.files?.length ?? 0) > 5) throw new Error("qgrid.image: at most five reference images are supported");
|
|
700
|
-
const
|
|
701
|
-
if (
|
|
702
|
-
|
|
703
|
-
if (qgridOptions.outputFormat !== void 0 && qgridOptions.outputFormat !== "png") throw new Error("qgrid.image: the subscription Images endpoint supports PNG output only");
|
|
704
|
-
if (qgridOptions.quality !== void 0 && ![
|
|
705
|
-
"low",
|
|
706
|
-
"medium",
|
|
707
|
-
"high"
|
|
708
|
-
].includes(qgridOptions.quality)) throw new Error("qgrid.image: quality must be low, medium, or high");
|
|
709
|
-
if (qgridOptions.background !== void 0 && ![
|
|
710
|
-
"auto",
|
|
711
|
-
"opaque",
|
|
712
|
-
"transparent"
|
|
713
|
-
].includes(qgridOptions.background)) throw new Error("qgrid.image: background must be auto, opaque, or transparent");
|
|
1178
|
+
const parsedQgridOptions = openAIImageQgridOptionsSchema.safeParse(options.providerOptions.qgrid ?? {});
|
|
1179
|
+
if (!parsedQgridOptions.success) throw new Error(`qgrid.image: ${parsedQgridOptions.error.issues[0]?.message}`);
|
|
1180
|
+
const qgridOptions = parsedQgridOptions.data;
|
|
714
1181
|
if (options.size !== void 0 && !/^[1-9]\d*x[1-9]\d*$/.test(options.size)) throw new Error("qgrid.image: size must be positive integer WIDTHxHEIGHT");
|
|
715
1182
|
const imageUrls = (options.files ?? []).map(imageInputUrl);
|
|
716
1183
|
assertImageInputFitsJsonTransport(imageUrls);
|
|
@@ -728,7 +1195,7 @@ function createQgridImage(modelId, config) {
|
|
|
728
1195
|
feature,
|
|
729
1196
|
details: "The subscription Images endpoint does not support this setting. Describe the desired result in the prompt."
|
|
730
1197
|
});
|
|
731
|
-
for (const feature of [...options.size
|
|
1198
|
+
for (const feature of [...options.size === void 0 ? [] : ["size"], ...qgridOptions.quality === void 0 ? [] : ["providerOptions.qgrid.quality"]]) warnings.push({
|
|
732
1199
|
type: "compatibility",
|
|
733
1200
|
feature,
|
|
734
1201
|
details: "This is a hint only. The subscription backend may choose a different size or quality; inspect the returned generation metadata."
|
|
@@ -737,53 +1204,753 @@ function createQgridImage(modelId, config) {
|
|
|
737
1204
|
const headers = new Headers();
|
|
738
1205
|
for (const [key, value] of Object.entries(options.headers ?? {})) if (value !== void 0) headers.set(key, value);
|
|
739
1206
|
headers.set("Content-Type", "application/json");
|
|
1207
|
+
const imageGenerationOptions = {};
|
|
1208
|
+
if (options.size !== void 0) imageGenerationOptions.size = options.size;
|
|
1209
|
+
if (qgridOptions.quality !== void 0) imageGenerationOptions.quality = qgridOptions.quality;
|
|
1210
|
+
if (qgridOptions.background !== void 0) imageGenerationOptions.background = qgridOptions.background;
|
|
1211
|
+
if (qgridOptions.outputFormat !== void 0) imageGenerationOptions.outputFormat = qgridOptions.outputFormat;
|
|
1212
|
+
const args = {
|
|
1213
|
+
prompt: options.prompt,
|
|
1214
|
+
model: modelId,
|
|
1215
|
+
imageGeneration: true,
|
|
1216
|
+
imageGenerationOptions
|
|
1217
|
+
};
|
|
1218
|
+
if (imageUrls.length) args.input = input;
|
|
1219
|
+
if (projectName) args.projectName = projectName;
|
|
1220
|
+
if (qgridOptions.tokenName !== void 0) args.tokenName = qgridOptions.tokenName;
|
|
1221
|
+
if (qgridOptions.logger === false) args.logger = false;
|
|
740
1222
|
const response = await qgridFetch(`${serverUrl}/api/qgrid/query`, {
|
|
741
1223
|
method: "POST",
|
|
742
1224
|
headers,
|
|
743
1225
|
signal: options.abortSignal,
|
|
744
|
-
body: JSON.stringify({ args
|
|
745
|
-
prompt: options.prompt,
|
|
746
|
-
model: modelId,
|
|
747
|
-
imageGeneration: true,
|
|
748
|
-
imageGenerationOptions: {
|
|
749
|
-
...options.size !== void 0 ? { size: options.size } : {},
|
|
750
|
-
...qgridOptions.quality !== void 0 ? { quality: qgridOptions.quality } : {},
|
|
751
|
-
...qgridOptions.background !== void 0 ? { background: qgridOptions.background } : {},
|
|
752
|
-
...qgridOptions.outputFormat !== void 0 ? { outputFormat: qgridOptions.outputFormat } : {}
|
|
753
|
-
},
|
|
754
|
-
...imageUrls.length ? { input } : {},
|
|
755
|
-
...projectName ? { projectName } : {},
|
|
756
|
-
...qgridOptions.tokenName !== void 0 ? { tokenName: qgridOptions.tokenName } : {},
|
|
757
|
-
...qgridOptions.logger === false ? { logger: false } : {}
|
|
758
|
-
} })
|
|
1226
|
+
body: JSON.stringify({ args })
|
|
759
1227
|
}, {
|
|
760
1228
|
operation: "query",
|
|
761
1229
|
serverUrl
|
|
762
1230
|
});
|
|
763
1231
|
if (!response.ok) throw new Error(`qgrid ${response.status}: ${await response.text().catch(() => "")}`);
|
|
764
|
-
const data = await response.json();
|
|
1232
|
+
const data = queryOutputSchema.parse(await response.json());
|
|
765
1233
|
const images = data.content?.filter((item) => item.type === "image") ?? [];
|
|
766
1234
|
const image = images[0];
|
|
767
1235
|
if (images.length !== 1 || !image?.data || image.generation?.route !== "codex-images") throw new Error("qgrid.image: expected one image from the direct subscription Images route; update the qgrid server and SDK together");
|
|
768
1236
|
const usage = image.generation.usage;
|
|
769
|
-
|
|
1237
|
+
const imageMetadata = {};
|
|
1238
|
+
if (data.requestLogId !== void 0) imageMetadata.requestLogId = data.requestLogId;
|
|
1239
|
+
imageMetadata.generation = image.generation;
|
|
1240
|
+
if (image.revisedPrompt !== null && image.revisedPrompt !== void 0) imageMetadata.revisedPrompt = image.revisedPrompt;
|
|
1241
|
+
const result = {
|
|
770
1242
|
images: [image.data],
|
|
771
1243
|
warnings,
|
|
772
1244
|
response: {
|
|
773
1245
|
timestamp,
|
|
774
|
-
modelId: image.generation.model ?? data.model,
|
|
1246
|
+
modelId: image.generation.model ?? data.model ?? modelId,
|
|
775
1247
|
headers: Object.fromEntries(response.headers)
|
|
776
1248
|
},
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1249
|
+
providerMetadata: { qgrid: { images: [imageMetadata] } }
|
|
1250
|
+
};
|
|
1251
|
+
if (usage) result.usage = {
|
|
1252
|
+
inputTokens: usage.input_tokens,
|
|
1253
|
+
outputTokens: usage.output_tokens,
|
|
1254
|
+
totalTokens: usage.total_tokens
|
|
1255
|
+
};
|
|
1256
|
+
return result;
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
const GROK_MAX_IMAGES_PER_CALL = 3;
|
|
1261
|
+
/** `providerOptions.xai` for Grok images, field-for-field from `@ai-sdk/xai` `xaiImageModelOptions`. */
|
|
1262
|
+
const xaiImageOptionsSchema = z.object({
|
|
1263
|
+
aspect_ratio: z.string().optional(),
|
|
1264
|
+
output_format: z.string().optional(),
|
|
1265
|
+
sync_mode: z.boolean().optional(),
|
|
1266
|
+
resolution: z.enum([
|
|
1267
|
+
"1k",
|
|
1268
|
+
"1.5k",
|
|
1269
|
+
"2k"
|
|
1270
|
+
]).optional(),
|
|
1271
|
+
quality: z.enum([
|
|
1272
|
+
"low",
|
|
1273
|
+
"medium",
|
|
1274
|
+
"high",
|
|
1275
|
+
"auto"
|
|
1276
|
+
]).optional(),
|
|
1277
|
+
user: z.string().optional()
|
|
1278
|
+
});
|
|
1279
|
+
const XAI_IMAGE_HONORED = /* @__PURE__ */ new Set([
|
|
1280
|
+
"aspect_ratio",
|
|
1281
|
+
"output_format",
|
|
1282
|
+
"resolution",
|
|
1283
|
+
"quality"
|
|
1284
|
+
]);
|
|
1285
|
+
const GROK_OUTPUT_FORMATS = [
|
|
1286
|
+
"png",
|
|
1287
|
+
"jpeg",
|
|
1288
|
+
"webp"
|
|
1289
|
+
];
|
|
1290
|
+
function isGrokOutputFormat(value) {
|
|
1291
|
+
return GROK_OUTPUT_FORMATS.some((format) => format === value);
|
|
1292
|
+
}
|
|
1293
|
+
const generateImagesOutputSchema = z.object({
|
|
1294
|
+
requestLogId: z.number().optional(),
|
|
1295
|
+
model: z.string(),
|
|
1296
|
+
tokenName: z.string(),
|
|
1297
|
+
images: z.array(z.object({
|
|
1298
|
+
b64: z.string(),
|
|
1299
|
+
mimeType: z.string(),
|
|
1300
|
+
revisedPrompt: z.string().optional()
|
|
1301
|
+
})),
|
|
1302
|
+
usage: z.object({
|
|
1303
|
+
imageCount: z.number(),
|
|
1304
|
+
costInUsdTicks: z.number().optional()
|
|
1305
|
+
}),
|
|
1306
|
+
costUsd: z.number().optional(),
|
|
1307
|
+
costSource: z.enum(["provider", "pricing_table"]).optional()
|
|
1308
|
+
});
|
|
1309
|
+
function createGrokImage(modelId, config) {
|
|
1310
|
+
const label = "qgrid.image";
|
|
1311
|
+
assertGrokModelId(modelId, label);
|
|
1312
|
+
const { serverUrl, projectName } = resolveMediaConfig(config);
|
|
1313
|
+
return {
|
|
1314
|
+
specificationVersion: "v3",
|
|
1315
|
+
provider: QGRID_PROVIDER_NAME,
|
|
1316
|
+
modelId,
|
|
1317
|
+
maxImagesPerCall: GROK_MAX_IMAGES_PER_CALL,
|
|
1318
|
+
async doGenerate(options) {
|
|
1319
|
+
if (!options.prompt?.trim()) throw new Error(`${label}: a non-empty text prompt is required`);
|
|
1320
|
+
if (!Number.isInteger(options.n) || options.n < 1 || options.n > GROK_MAX_IMAGES_PER_CALL) throw new Error(`${label}: Grok returns 1 to ${GROK_MAX_IMAGES_PER_CALL} images per call; omit maxImagesPerCall so AI SDK can batch`);
|
|
1321
|
+
const files = options.files ?? [];
|
|
1322
|
+
if (files.length > 5) throw new Error(`${label}: at most five reference images are supported`);
|
|
1323
|
+
const qgridOptions = parseGrokQgridOptions(options.providerOptions.qgrid, label);
|
|
1324
|
+
const xai = parseXaiMediaOptions(options.providerOptions.xai, xaiImageOptionsSchema, XAI_IMAGE_HONORED, label);
|
|
1325
|
+
const outputFormat = xai.options?.output_format;
|
|
1326
|
+
if (outputFormat !== void 0 && !isGrokOutputFormat(outputFormat)) throw new Error(`${label}: providerOptions.xai.output_format must be png, jpeg, or webp`);
|
|
1327
|
+
const warnings = [];
|
|
1328
|
+
if (options.size !== void 0) warnings.push({
|
|
1329
|
+
type: "unsupported",
|
|
1330
|
+
feature: "size",
|
|
1331
|
+
details: "This model does not support the `size` option. Use `aspectRatio` instead."
|
|
1332
|
+
});
|
|
1333
|
+
if (options.seed !== void 0) warnings.push({
|
|
1334
|
+
type: "unsupported",
|
|
1335
|
+
feature: "seed"
|
|
1336
|
+
});
|
|
1337
|
+
if (options.mask !== void 0) warnings.push({
|
|
1338
|
+
type: "unsupported",
|
|
1339
|
+
feature: "mask"
|
|
1340
|
+
});
|
|
1341
|
+
warnings.push(...xai.warnings);
|
|
1342
|
+
const referenceImages = files.map((file) => toImageRef(file, label));
|
|
1343
|
+
assertInlineMediaFits(referenceImages, label);
|
|
1344
|
+
const aspectRatio = options.aspectRatio ?? xai.options?.aspect_ratio;
|
|
1345
|
+
const params = {
|
|
1346
|
+
model: modelId,
|
|
1347
|
+
prompt: options.prompt,
|
|
1348
|
+
n: options.n
|
|
1349
|
+
};
|
|
1350
|
+
if (aspectRatio !== void 0) params.aspectRatio = aspectRatio;
|
|
1351
|
+
if (xai.options?.resolution !== void 0) params.resolution = xai.options.resolution;
|
|
1352
|
+
if (xai.options?.quality !== void 0) params.quality = xai.options.quality;
|
|
1353
|
+
if (outputFormat !== void 0) params.outputFormat = outputFormat;
|
|
1354
|
+
if (referenceImages.length) params.referenceImages = referenceImages;
|
|
1355
|
+
if (projectName) params.projectName = projectName;
|
|
1356
|
+
const body = serializeMediaBody({
|
|
1357
|
+
...params,
|
|
1358
|
+
...qgridOptions
|
|
1359
|
+
}, label);
|
|
1360
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
1361
|
+
const { data, headers } = await postMediaEndpoint({
|
|
1362
|
+
serverUrl,
|
|
1363
|
+
endpoint: "generateImages",
|
|
1364
|
+
body,
|
|
1365
|
+
schema: generateImagesOutputSchema,
|
|
1366
|
+
label,
|
|
1367
|
+
signal: options.abortSignal,
|
|
1368
|
+
headers: options.headers
|
|
1369
|
+
});
|
|
1370
|
+
if (data.images.length === 0) throw new Error(`${label}: the Grok backend returned no images`);
|
|
1371
|
+
const costInUsdTicks = data.usage.costInUsdTicks;
|
|
1372
|
+
const callCost = {};
|
|
1373
|
+
if (data.costUsd !== void 0) callCost.costUsd = data.costUsd;
|
|
1374
|
+
if (data.costSource !== void 0) callCost.costSource = data.costSource;
|
|
1375
|
+
const xaiImages = data.images.map((image, index) => {
|
|
1376
|
+
const entry = {};
|
|
1377
|
+
if (image.revisedPrompt) entry.revisedPrompt = image.revisedPrompt;
|
|
1378
|
+
if (index === 0 && costInUsdTicks !== void 0) entry.costInUsdTicks = costInUsdTicks;
|
|
1379
|
+
return entry;
|
|
1380
|
+
});
|
|
1381
|
+
const xaiCallCost = {};
|
|
1382
|
+
if (costInUsdTicks !== void 0) xaiCallCost.costInUsdTicks = costInUsdTicks;
|
|
1383
|
+
const qgridImages = data.images.map((image, index) => {
|
|
1384
|
+
const entry = {
|
|
1385
|
+
mediaType: image.mimeType,
|
|
1386
|
+
tokenName: data.tokenName
|
|
1387
|
+
};
|
|
1388
|
+
if (data.requestLogId !== void 0) entry.requestLogId = data.requestLogId;
|
|
1389
|
+
return index === 0 ? {
|
|
1390
|
+
...entry,
|
|
1391
|
+
...callCost
|
|
1392
|
+
} : entry;
|
|
1393
|
+
});
|
|
1394
|
+
const qgridCallInfo = {};
|
|
1395
|
+
if (data.requestLogId !== void 0) qgridCallInfo.requestLogId = data.requestLogId;
|
|
1396
|
+
qgridCallInfo.tokenName = data.tokenName;
|
|
1397
|
+
return {
|
|
1398
|
+
images: data.images.map((image) => image.b64),
|
|
1399
|
+
warnings,
|
|
1400
|
+
response: {
|
|
1401
|
+
timestamp,
|
|
1402
|
+
modelId: data.model,
|
|
1403
|
+
headers
|
|
1404
|
+
},
|
|
1405
|
+
providerMetadata: {
|
|
1406
|
+
xai: {
|
|
1407
|
+
images: xaiImages,
|
|
1408
|
+
...xaiCallCost
|
|
1409
|
+
},
|
|
1410
|
+
qgrid: {
|
|
1411
|
+
images: qgridImages,
|
|
1412
|
+
...qgridCallInfo,
|
|
1413
|
+
...callCost
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
//#endregion
|
|
1421
|
+
//#region src/speech-model.ts
|
|
1422
|
+
const LABEL$2 = "qgrid.speech";
|
|
1423
|
+
const MAX_TEXT_CHARS = 15e3;
|
|
1424
|
+
/** Codecs of `@ai-sdk/xai` `XaiSpeechCodec`, equal to the qgrid server's `format` enum. */
|
|
1425
|
+
const SPEECH_FORMATS = [
|
|
1426
|
+
"mp3",
|
|
1427
|
+
"wav",
|
|
1428
|
+
"pcm",
|
|
1429
|
+
"mulaw",
|
|
1430
|
+
"alaw"
|
|
1431
|
+
];
|
|
1432
|
+
function isSpeechFormat(value) {
|
|
1433
|
+
return SPEECH_FORMATS.some((format) => format === value);
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* `providerOptions.xai` for Grok speech, field-for-field from `@ai-sdk/xai`
|
|
1437
|
+
* `xaiSpeechModelOptionsSchema`. The qgrid server takes none of them; every set key warns.
|
|
1438
|
+
*/
|
|
1439
|
+
const xaiSpeechOptionsSchema = z.object({
|
|
1440
|
+
sampleRate: z.union([
|
|
1441
|
+
z.literal(8e3),
|
|
1442
|
+
z.literal(16e3),
|
|
1443
|
+
z.literal(22050),
|
|
1444
|
+
z.literal(24e3),
|
|
1445
|
+
z.literal(44100),
|
|
1446
|
+
z.literal(48e3)
|
|
1447
|
+
]).nullish(),
|
|
1448
|
+
bitRate: z.union([
|
|
1449
|
+
z.literal(32e3),
|
|
1450
|
+
z.literal(64e3),
|
|
1451
|
+
z.literal(96e3),
|
|
1452
|
+
z.literal(128e3),
|
|
1453
|
+
z.literal(192e3)
|
|
1454
|
+
]).nullish(),
|
|
1455
|
+
optimizeStreamingLatency: z.union([
|
|
1456
|
+
z.literal(0),
|
|
1457
|
+
z.literal(1),
|
|
1458
|
+
z.literal(2)
|
|
1459
|
+
]).nullish(),
|
|
1460
|
+
textNormalization: z.boolean().nullish(),
|
|
1461
|
+
withTimestamps: z.boolean().nullish(),
|
|
1462
|
+
replace: z.record(z.string(), z.string()).nullish()
|
|
1463
|
+
});
|
|
1464
|
+
const speakOutputSchema = z.object({
|
|
1465
|
+
audio: z.string(),
|
|
1466
|
+
mimeType: z.string(),
|
|
1467
|
+
tokenName: z.string(),
|
|
1468
|
+
requestLogId: z.number().optional()
|
|
1469
|
+
});
|
|
1470
|
+
function base64ToBytes(data) {
|
|
1471
|
+
if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(data, "base64"));
|
|
1472
|
+
const binary = atob(data);
|
|
1473
|
+
const bytes = new Uint8Array(binary.length);
|
|
1474
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
1475
|
+
return bytes;
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Grok text-to-speech through `POST /api/qgrid/speak`. `voice`, `language` and `outputFormat`
|
|
1479
|
+
* map to the server's `voice`, `language` and `format`; when omitted the server applies
|
|
1480
|
+
* `ara` / `en` / `mp3` (the only combination verified live). The model id only labels the log.
|
|
1481
|
+
*/
|
|
1482
|
+
function createQgridSpeech(modelId, config) {
|
|
1483
|
+
assertGrokModelId(modelId, LABEL$2);
|
|
1484
|
+
const { serverUrl, projectName } = resolveMediaConfig(config);
|
|
1485
|
+
return {
|
|
1486
|
+
specificationVersion: "v3",
|
|
1487
|
+
provider: QGRID_PROVIDER_NAME,
|
|
1488
|
+
modelId,
|
|
1489
|
+
async doGenerate(options) {
|
|
1490
|
+
if (!options.text.trim()) throw new Error(`${LABEL$2}: text must not be empty`);
|
|
1491
|
+
if (options.text.length > MAX_TEXT_CHARS) throw new Error(`${LABEL$2}: text is limited to ${MAX_TEXT_CHARS} characters`);
|
|
1492
|
+
const qgridOptions = parseGrokQgridOptions(options.providerOptions?.qgrid, LABEL$2);
|
|
1493
|
+
const xai = parseXaiMediaOptions(options.providerOptions?.xai, xaiSpeechOptionsSchema, /* @__PURE__ */ new Set(), LABEL$2);
|
|
1494
|
+
const warnings = [];
|
|
1495
|
+
let format;
|
|
1496
|
+
if (options.outputFormat !== void 0) if (isSpeechFormat(options.outputFormat)) format = options.outputFormat;
|
|
1497
|
+
else {
|
|
1498
|
+
format = "mp3";
|
|
1499
|
+
warnings.push({
|
|
1500
|
+
type: "unsupported",
|
|
1501
|
+
feature: "outputFormat",
|
|
1502
|
+
details: `Unsupported output format: ${options.outputFormat}. Using mp3 instead.`
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
if (options.instructions !== void 0) warnings.push({
|
|
1506
|
+
type: "unsupported",
|
|
1507
|
+
feature: "instructions"
|
|
1508
|
+
});
|
|
1509
|
+
if (options.speed !== void 0) warnings.push({
|
|
1510
|
+
type: "unsupported",
|
|
1511
|
+
feature: "speed"
|
|
1512
|
+
});
|
|
1513
|
+
warnings.push(...xai.warnings);
|
|
1514
|
+
const requestParams = {
|
|
1515
|
+
model: modelId,
|
|
1516
|
+
text: options.text
|
|
1517
|
+
};
|
|
1518
|
+
if (options.voice !== void 0) requestParams.voice = options.voice;
|
|
1519
|
+
if (options.language !== void 0) requestParams.language = options.language;
|
|
1520
|
+
if (format !== void 0) requestParams.format = format;
|
|
1521
|
+
if (projectName) requestParams.projectName = projectName;
|
|
1522
|
+
const params = {
|
|
1523
|
+
...requestParams,
|
|
1524
|
+
...qgridOptions
|
|
1525
|
+
};
|
|
1526
|
+
const body = serializeMediaBody(params, LABEL$2);
|
|
1527
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
1528
|
+
const { data, headers } = await postMediaEndpoint({
|
|
1529
|
+
serverUrl,
|
|
1530
|
+
endpoint: "speak",
|
|
1531
|
+
body,
|
|
1532
|
+
schema: speakOutputSchema,
|
|
1533
|
+
label: LABEL$2,
|
|
1534
|
+
signal: options.abortSignal,
|
|
1535
|
+
headers: options.headers
|
|
1536
|
+
});
|
|
1537
|
+
const qgridMetadata = {
|
|
1538
|
+
mediaType: data.mimeType,
|
|
1539
|
+
tokenName: data.tokenName
|
|
1540
|
+
};
|
|
1541
|
+
if (data.requestLogId !== void 0) qgridMetadata.requestLogId = data.requestLogId;
|
|
1542
|
+
return {
|
|
1543
|
+
audio: base64ToBytes(data.audio),
|
|
1544
|
+
warnings,
|
|
1545
|
+
request: { body: params },
|
|
1546
|
+
response: {
|
|
1547
|
+
timestamp,
|
|
1548
|
+
modelId,
|
|
1549
|
+
headers
|
|
1550
|
+
},
|
|
1551
|
+
providerMetadata: { qgrid: qgridMetadata }
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
//#endregion
|
|
1557
|
+
//#region src/transcription-model.ts
|
|
1558
|
+
const LABEL$1 = "qgrid.transcription";
|
|
1559
|
+
const AUDIO_MEDIA_TYPE = /^audio\/[A-Za-z0-9.+-]+$/;
|
|
1560
|
+
/**
|
|
1561
|
+
* `providerOptions.xai` for Grok transcription, field-for-field from `@ai-sdk/xai`
|
|
1562
|
+
* `xaiTranscriptionModelOptionsSchema`. Only `language` reaches the qgrid server.
|
|
1563
|
+
*/
|
|
1564
|
+
const xaiTranscriptionOptionsSchema = z.object({
|
|
1565
|
+
audioFormat: z.enum([
|
|
1566
|
+
"pcm",
|
|
1567
|
+
"mulaw",
|
|
1568
|
+
"alaw",
|
|
1569
|
+
"opus"
|
|
1570
|
+
]).nullish(),
|
|
1571
|
+
sampleRate: z.union([
|
|
1572
|
+
z.literal(8e3),
|
|
1573
|
+
z.literal(16e3),
|
|
1574
|
+
z.literal(22050),
|
|
1575
|
+
z.literal(24e3),
|
|
1576
|
+
z.literal(44100),
|
|
1577
|
+
z.literal(48e3)
|
|
1578
|
+
]).nullish(),
|
|
1579
|
+
language: z.string().min(1).max(16).nullish(),
|
|
1580
|
+
format: z.boolean().nullish(),
|
|
1581
|
+
multichannel: z.boolean().nullish(),
|
|
1582
|
+
channels: z.number().int().min(2).max(8).nullish(),
|
|
1583
|
+
diarize: z.boolean().nullish(),
|
|
1584
|
+
keyterm: z.union([z.string(), z.array(z.string())]).nullish(),
|
|
1585
|
+
fillerWords: z.boolean().nullish(),
|
|
1586
|
+
vadThreshold: z.number().min(0).max(1).optional(),
|
|
1587
|
+
streaming: z.object({}).loose().optional()
|
|
1588
|
+
});
|
|
1589
|
+
const XAI_TRANSCRIPTION_HONORED = /* @__PURE__ */ new Set(["language"]);
|
|
1590
|
+
const transcribeOutputSchema = z.object({
|
|
1591
|
+
text: z.string(),
|
|
1592
|
+
language: z.string().optional(),
|
|
1593
|
+
durationSec: z.number().optional(),
|
|
1594
|
+
model: z.string(),
|
|
1595
|
+
tokenName: z.string(),
|
|
1596
|
+
requestLogId: z.number().optional()
|
|
1597
|
+
});
|
|
1598
|
+
function normalizeMediaType(mediaType) {
|
|
1599
|
+
const bare = mediaType.split(";")[0].trim().toLowerCase();
|
|
1600
|
+
if (!AUDIO_MEDIA_TYPE.test(bare)) throw new Error(`${LABEL$1}: mediaType must be audio/*, got "${mediaType}"`);
|
|
1601
|
+
return bare;
|
|
1602
|
+
}
|
|
1603
|
+
function createQgridTranscription(modelId, config) {
|
|
1604
|
+
assertGrokModelId(modelId, LABEL$1);
|
|
1605
|
+
const { serverUrl, projectName } = resolveMediaConfig(config);
|
|
1606
|
+
return {
|
|
1607
|
+
specificationVersion: "v3",
|
|
1608
|
+
provider: QGRID_PROVIDER_NAME,
|
|
1609
|
+
modelId,
|
|
1610
|
+
async doGenerate(options) {
|
|
1611
|
+
const mediaType = normalizeMediaType(options.mediaType);
|
|
1612
|
+
const audio = options.audio instanceof Uint8Array ? bytesToBase64(options.audio) : options.audio;
|
|
1613
|
+
if (audio.length === 0) throw new Error(`${LABEL$1}: audio is empty`);
|
|
1614
|
+
assertInlineMediaFits([audio], LABEL$1);
|
|
1615
|
+
const qgridOptions = parseGrokQgridOptions(options.providerOptions?.qgrid, LABEL$1);
|
|
1616
|
+
const xai = parseXaiMediaOptions(options.providerOptions?.xai, xaiTranscriptionOptionsSchema, XAI_TRANSCRIPTION_HONORED, LABEL$1);
|
|
1617
|
+
const warnings = [...xai.warnings];
|
|
1618
|
+
const language = xai.options?.language ?? void 0;
|
|
1619
|
+
const params = {
|
|
1620
|
+
model: modelId,
|
|
1621
|
+
audio,
|
|
1622
|
+
mediaType
|
|
1623
|
+
};
|
|
1624
|
+
if (language !== void 0) params.language = language;
|
|
1625
|
+
if (projectName) params.projectName = projectName;
|
|
1626
|
+
const body = serializeMediaBody({
|
|
1627
|
+
...params,
|
|
1628
|
+
...qgridOptions
|
|
1629
|
+
}, LABEL$1);
|
|
1630
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
1631
|
+
const { data, headers } = await postMediaEndpoint({
|
|
1632
|
+
serverUrl,
|
|
1633
|
+
endpoint: "transcribe",
|
|
1634
|
+
body,
|
|
1635
|
+
schema: transcribeOutputSchema,
|
|
1636
|
+
label: LABEL$1,
|
|
1637
|
+
signal: options.abortSignal,
|
|
1638
|
+
headers: options.headers
|
|
1639
|
+
});
|
|
1640
|
+
const qgridMetadata = { tokenName: data.tokenName };
|
|
1641
|
+
if (data.requestLogId !== void 0) qgridMetadata.requestLogId = data.requestLogId;
|
|
1642
|
+
return {
|
|
1643
|
+
text: data.text,
|
|
1644
|
+
segments: [],
|
|
1645
|
+
language: data.language,
|
|
1646
|
+
durationInSeconds: data.durationSec,
|
|
1647
|
+
warnings,
|
|
1648
|
+
response: {
|
|
1649
|
+
timestamp,
|
|
1650
|
+
modelId: data.model,
|
|
1651
|
+
headers
|
|
1652
|
+
},
|
|
1653
|
+
providerMetadata: { qgrid: qgridMetadata }
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
//#endregion
|
|
1659
|
+
//#region src/video-model.ts
|
|
1660
|
+
const LABEL = "qgrid.video";
|
|
1661
|
+
const MAX_CONSECUTIVE_POLL_FAILURES = 3;
|
|
1662
|
+
const MAX_REFERENCE_IMAGES = 5;
|
|
1663
|
+
const LAST_FRAME_MODEL = "grok/grok-imagine-video-1.5";
|
|
1664
|
+
const RESOLUTION_MAP = /* @__PURE__ */ new Map([
|
|
1665
|
+
["1920x1080", "1080p"],
|
|
1666
|
+
["1280x720", "720p"],
|
|
1667
|
+
["854x480", "480p"],
|
|
1668
|
+
["640x480", "480p"]
|
|
1669
|
+
]);
|
|
1670
|
+
/**
|
|
1671
|
+
* `providerOptions.xai` for Grok video. Field names follow `@ai-sdk/xai` `xaiVideoModelOptions`;
|
|
1672
|
+
* `lastFrame` is a qgrid extension because AI SDK v3 video calls have no `frameImages`.
|
|
1673
|
+
* Edit / extend modes need a source video URL the qgrid server does not accept, so they throw.
|
|
1674
|
+
*/
|
|
1675
|
+
const xaiVideoOptionsSchema = z.looseObject({
|
|
1676
|
+
mode: z.enum([
|
|
1677
|
+
"edit-video",
|
|
1678
|
+
"extend-video",
|
|
1679
|
+
"reference-to-video"
|
|
1680
|
+
]).optional(),
|
|
1681
|
+
videoUrl: z.string().min(1).optional(),
|
|
1682
|
+
referenceImageUrls: z.array(z.string().min(1)).min(1).max(7).optional(),
|
|
1683
|
+
referenceVoiceIds: z.array(z.string().min(1)).max(3).optional(),
|
|
1684
|
+
keyframes: z.array(z.unknown()).optional(),
|
|
1685
|
+
storageOptions: z.unknown().optional(),
|
|
1686
|
+
user: z.string().optional(),
|
|
1687
|
+
pollIntervalMs: z.number().positive().nullish(),
|
|
1688
|
+
pollTimeoutMs: z.number().positive().nullish(),
|
|
1689
|
+
resolution: z.enum([
|
|
1690
|
+
"480p",
|
|
1691
|
+
"720p",
|
|
1692
|
+
"1080p"
|
|
1693
|
+
]).nullish(),
|
|
1694
|
+
lastFrame: z.string().min(1).optional()
|
|
1695
|
+
});
|
|
1696
|
+
const XAI_VIDEO_HONORED = /* @__PURE__ */ new Set([
|
|
1697
|
+
"mode",
|
|
1698
|
+
"referenceImageUrls",
|
|
1699
|
+
"resolution",
|
|
1700
|
+
"lastFrame",
|
|
1701
|
+
"pollIntervalMs",
|
|
1702
|
+
"pollTimeoutMs"
|
|
1703
|
+
]);
|
|
1704
|
+
const videoStartOutputSchema = z.object({
|
|
1705
|
+
jobHandle: z.string(),
|
|
1706
|
+
model: z.string(),
|
|
1707
|
+
requestLogId: z.number().optional()
|
|
1708
|
+
});
|
|
1709
|
+
const videoStatusOutputSchema = z.object({
|
|
1710
|
+
status: z.enum([
|
|
1711
|
+
"pending",
|
|
1712
|
+
"done",
|
|
1713
|
+
"failed"
|
|
1714
|
+
]),
|
|
1715
|
+
url: z.string().optional(),
|
|
1716
|
+
mimeType: z.literal("video/mp4"),
|
|
1717
|
+
durationSec: z.number().optional(),
|
|
1718
|
+
costInUsdTicks: z.number().optional(),
|
|
1719
|
+
costUsd: z.number().optional(),
|
|
1720
|
+
error: z.string().optional(),
|
|
1721
|
+
requestLogId: z.number().optional()
|
|
1722
|
+
});
|
|
1723
|
+
/**
|
|
1724
|
+
* Raised when the poll budget runs out. The job keeps running on xAI; pass `jobHandle` to
|
|
1725
|
+
* `resumeQgridVideo` (with the same `projectName`) to keep waiting.
|
|
1726
|
+
*/
|
|
1727
|
+
var QgridVideoTimeoutError = class extends Error {
|
|
1728
|
+
jobHandle;
|
|
1729
|
+
constructor(jobHandle, timeoutMs) {
|
|
1730
|
+
super(`${LABEL}: video job ${jobHandle} did not finish within ${timeoutMs} ms; resume it with resumeQgridVideo("${jobHandle}")`);
|
|
1731
|
+
this.name = "QgridVideoTimeoutError";
|
|
1732
|
+
this.jobHandle = jobHandle;
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
/** The upstream job finished as failed (including content-policy blocks). */
|
|
1736
|
+
var QgridVideoGenerationError = class extends Error {
|
|
1737
|
+
jobHandle;
|
|
1738
|
+
constructor(jobHandle, detail) {
|
|
1739
|
+
super(`${LABEL}: video generation failed${detail ? `: ${detail}` : "."}`);
|
|
1740
|
+
this.name = "QgridVideoGenerationError";
|
|
1741
|
+
this.jobHandle = jobHandle;
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
1744
|
+
function abortedError() {
|
|
1745
|
+
return new DOMException("The operation was aborted.", "AbortError");
|
|
1746
|
+
}
|
|
1747
|
+
function sleep(ms, signal) {
|
|
1748
|
+
return new Promise((resolve, reject) => {
|
|
1749
|
+
if (signal?.aborted) {
|
|
1750
|
+
reject(signal.reason ?? abortedError());
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
const onAbort = () => {
|
|
1754
|
+
clearTimeout(timer);
|
|
1755
|
+
reject(signal?.reason ?? abortedError());
|
|
1756
|
+
};
|
|
1757
|
+
const timer = setTimeout(() => {
|
|
1758
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1759
|
+
resolve();
|
|
1760
|
+
}, ms);
|
|
1761
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
function isTransientPollError(cause) {
|
|
1765
|
+
if (cause instanceof QgridMediaHttpError) return cause.status === 429 || cause.status >= 500;
|
|
1766
|
+
return cause instanceof Error && /transport failed/.test(cause.message);
|
|
1767
|
+
}
|
|
1768
|
+
function pollDeadline(caller, ms) {
|
|
1769
|
+
const controller = new AbortController();
|
|
1770
|
+
const onCallerAbort = () => controller.abort(caller?.reason ?? abortedError());
|
|
1771
|
+
if (caller?.aborted) onCallerAbort();
|
|
1772
|
+
else caller?.addEventListener("abort", onCallerAbort, { once: true });
|
|
1773
|
+
const deadlineReason = new DOMException("The video poll budget elapsed.", "TimeoutError");
|
|
1774
|
+
const timer = setTimeout(() => controller.abort(deadlineReason), Math.max(0, ms));
|
|
1775
|
+
return {
|
|
1776
|
+
signal: controller.signal,
|
|
1777
|
+
expired: (cause) => controller.signal.reason === deadlineReason && cause === deadlineReason,
|
|
1778
|
+
dispose: () => {
|
|
1779
|
+
clearTimeout(timer);
|
|
1780
|
+
caller?.removeEventListener("abort", onCallerAbort);
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
async function pollVideo(jobHandle, modelId, config, options, extraMetadata) {
|
|
1785
|
+
const interval = options.pollIntervalMs ?? 5e3;
|
|
1786
|
+
const timeout = options.pollTimeoutMs ?? 3e5;
|
|
1787
|
+
const statusParams = { jobHandle };
|
|
1788
|
+
if (config.projectName) statusParams.projectName = config.projectName;
|
|
1789
|
+
const body = serializeMediaBody(statusParams, LABEL);
|
|
1790
|
+
const deadline = pollDeadline(options.abortSignal, timeout);
|
|
1791
|
+
try {
|
|
1792
|
+
return await pollUntilDone(jobHandle, modelId, config, options, extraMetadata, {
|
|
1793
|
+
body,
|
|
1794
|
+
interval,
|
|
1795
|
+
timeout,
|
|
1796
|
+
signal: deadline.signal
|
|
1797
|
+
});
|
|
1798
|
+
} catch (error) {
|
|
1799
|
+
if (deadline.expired(error)) throw new QgridVideoTimeoutError(jobHandle, timeout);
|
|
1800
|
+
throw error;
|
|
1801
|
+
} finally {
|
|
1802
|
+
deadline.dispose();
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
async function pollUntilDone(jobHandle, modelId, config, options, extraMetadata, poll) {
|
|
1806
|
+
const { body, interval, timeout, signal } = poll;
|
|
1807
|
+
const deadline = Date.now() + timeout;
|
|
1808
|
+
let failures = 0;
|
|
1809
|
+
for (;;) {
|
|
1810
|
+
signal.throwIfAborted();
|
|
1811
|
+
let polled;
|
|
1812
|
+
try {
|
|
1813
|
+
polled = await postMediaEndpoint({
|
|
1814
|
+
serverUrl: config.serverUrl,
|
|
1815
|
+
endpoint: "videoStatus",
|
|
1816
|
+
body,
|
|
1817
|
+
schema: videoStatusOutputSchema,
|
|
1818
|
+
label: LABEL,
|
|
1819
|
+
signal,
|
|
1820
|
+
headers: options.headers
|
|
1821
|
+
});
|
|
1822
|
+
failures = 0;
|
|
1823
|
+
} catch (error) {
|
|
1824
|
+
if (!isTransientPollError(error) || ++failures >= MAX_CONSECUTIVE_POLL_FAILURES) throw error;
|
|
1825
|
+
}
|
|
1826
|
+
if (polled) {
|
|
1827
|
+
const status = polled.data;
|
|
1828
|
+
if (status.status === "failed") throw new QgridVideoGenerationError(jobHandle, status.error);
|
|
1829
|
+
if (status.status === "done") {
|
|
1830
|
+
if (!status.url) throw new QgridVideoGenerationError(jobHandle, "completed without a video URL");
|
|
1831
|
+
const requestLogId = status.requestLogId ?? extraMetadata.requestLogId;
|
|
1832
|
+
const xaiMetadata = { videoUrl: status.url };
|
|
1833
|
+
if (status.durationSec !== void 0) xaiMetadata.duration = status.durationSec;
|
|
1834
|
+
if (status.costInUsdTicks !== void 0) xaiMetadata.costInUsdTicks = status.costInUsdTicks;
|
|
1835
|
+
const qgridMetadata = { jobHandle };
|
|
1836
|
+
if (requestLogId !== void 0) qgridMetadata.requestLogId = requestLogId;
|
|
1837
|
+
if (status.costUsd !== void 0) qgridMetadata.costUsd = status.costUsd;
|
|
1838
|
+
return {
|
|
1839
|
+
videos: [{
|
|
1840
|
+
type: "url",
|
|
1841
|
+
url: status.url,
|
|
1842
|
+
mediaType: "video/mp4"
|
|
1843
|
+
}],
|
|
1844
|
+
providerMetadata: {
|
|
1845
|
+
xai: xaiMetadata,
|
|
1846
|
+
qgrid: qgridMetadata
|
|
1847
|
+
},
|
|
1848
|
+
response: {
|
|
1849
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1850
|
+
modelId,
|
|
1851
|
+
headers: polled.headers
|
|
1852
|
+
}
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
const remaining = deadline - Date.now();
|
|
1857
|
+
if (remaining <= 0) throw new QgridVideoTimeoutError(jobHandle, timeout);
|
|
1858
|
+
await sleep(Math.min(interval, remaining), signal);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Keeps polling a video job started by `qgrid.video(...)`, for example after a
|
|
1863
|
+
* `QgridVideoTimeoutError`. `config.projectName` must equal the one used to start the job.
|
|
1864
|
+
* AI SDK v3 video models expose only `doGenerate` (no `doStart` / `doStatus`), so resuming is
|
|
1865
|
+
* a qgrid helper rather than an `experimental_generateVideo` option.
|
|
1866
|
+
*/
|
|
1867
|
+
function resumeQgridVideo(jobHandle, options = {}) {
|
|
1868
|
+
const config = resolveMediaConfig(options);
|
|
1869
|
+
return pollVideo(jobHandle, options.modelId ?? "grok/video", config, options, {});
|
|
1870
|
+
}
|
|
1871
|
+
function createQgridVideo(modelId, config) {
|
|
1872
|
+
assertGrokModelId(modelId, LABEL);
|
|
1873
|
+
const resolved = resolveMediaConfig(config);
|
|
1874
|
+
return {
|
|
1875
|
+
specificationVersion: "v3",
|
|
1876
|
+
provider: QGRID_PROVIDER_NAME,
|
|
1877
|
+
modelId,
|
|
1878
|
+
maxVideosPerCall: 1,
|
|
1879
|
+
async doGenerate(options) {
|
|
1880
|
+
if (options.n !== 1) throw new Error(`${LABEL}: one video per call; omit maxVideosPerCall so AI SDK can batch`);
|
|
1881
|
+
if (!options.prompt?.trim()) throw new Error(`${LABEL}: a non-empty text prompt is required`);
|
|
1882
|
+
if (options.duration !== void 0 && (!Number.isInteger(options.duration) || options.duration < 1 || options.duration > 15)) throw new Error(`${LABEL}: duration must be an integer from 1 to 15 seconds`);
|
|
1883
|
+
const qgridOptions = parseGrokQgridOptions(options.providerOptions.qgrid, LABEL);
|
|
1884
|
+
const xai = parseXaiMediaOptions(options.providerOptions.xai, xaiVideoOptionsSchema, XAI_VIDEO_HONORED, LABEL);
|
|
1885
|
+
const xaiOptions = xai.options;
|
|
1886
|
+
if (xaiOptions?.mode === "edit-video" || xaiOptions?.mode === "extend-video" || xaiOptions?.videoUrl !== void 0) throw new Error(`${LABEL}: video editing and extension (mode edit-video / extend-video, videoUrl) are not supported by qgrid`);
|
|
1887
|
+
const referenceImages = (xaiOptions?.referenceImageUrls ?? []).map((url) => toImageRefUrl(url, LABEL));
|
|
1888
|
+
if (referenceImages.length > MAX_REFERENCE_IMAGES) throw new Error(`${LABEL}: at most ${MAX_REFERENCE_IMAGES} reference images are supported`);
|
|
1889
|
+
if (xaiOptions?.mode === "reference-to-video" && referenceImages.length === 0) throw new Error(`${LABEL}: reference-to-video needs providerOptions.xai.referenceImageUrls`);
|
|
1890
|
+
const warnings = [...xai.warnings];
|
|
1891
|
+
if (options.fps !== void 0) warnings.push({
|
|
1892
|
+
type: "unsupported",
|
|
1893
|
+
feature: "fps"
|
|
1894
|
+
});
|
|
1895
|
+
if (options.seed !== void 0) warnings.push({
|
|
1896
|
+
type: "unsupported",
|
|
1897
|
+
feature: "seed"
|
|
1898
|
+
});
|
|
1899
|
+
let resolution = xaiOptions?.resolution ?? void 0;
|
|
1900
|
+
if (resolution === void 0 && options.resolution !== void 0) {
|
|
1901
|
+
resolution = RESOLUTION_MAP.get(options.resolution);
|
|
1902
|
+
if (resolution === void 0) warnings.push({
|
|
1903
|
+
type: "unsupported",
|
|
1904
|
+
feature: "resolution",
|
|
1905
|
+
details: `Unrecognized resolution "${options.resolution}". Use providerOptions.xai.resolution with "480p", "720p", or "1080p" instead.`
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
let lastFrame;
|
|
1909
|
+
if (xaiOptions?.lastFrame !== void 0) if (modelId === LAST_FRAME_MODEL) lastFrame = toImageRefUrl(xaiOptions.lastFrame, LABEL);
|
|
1910
|
+
else warnings.push({
|
|
1911
|
+
type: "unsupported",
|
|
1912
|
+
feature: "providerOptions.xai.lastFrame",
|
|
1913
|
+
details: `xAI only supports last_frame with "${LAST_FRAME_MODEL}". The last frame was ignored.`
|
|
1914
|
+
});
|
|
1915
|
+
const image = options.image === void 0 ? void 0 : toImageRef(options.image, LABEL);
|
|
1916
|
+
assertInlineMediaFits([
|
|
1917
|
+
image,
|
|
1918
|
+
lastFrame,
|
|
1919
|
+
...referenceImages
|
|
1920
|
+
], LABEL);
|
|
1921
|
+
const params = {
|
|
1922
|
+
model: modelId,
|
|
1923
|
+
prompt: options.prompt
|
|
1924
|
+
};
|
|
1925
|
+
if (options.duration !== void 0) params.duration = options.duration;
|
|
1926
|
+
if (options.aspectRatio !== void 0) params.aspectRatio = options.aspectRatio;
|
|
1927
|
+
if (resolution !== void 0) params.resolution = resolution;
|
|
1928
|
+
if (image !== void 0) params.image = image;
|
|
1929
|
+
if (lastFrame !== void 0) params.lastFrame = lastFrame;
|
|
1930
|
+
if (referenceImages.length) params.referenceImages = referenceImages;
|
|
1931
|
+
if (xaiOptions?.mode !== void 0) params.mode = xaiOptions.mode;
|
|
1932
|
+
if (resolved.projectName) params.projectName = resolved.projectName;
|
|
1933
|
+
const body = serializeMediaBody({
|
|
1934
|
+
...params,
|
|
1935
|
+
...qgridOptions
|
|
1936
|
+
}, LABEL);
|
|
1937
|
+
const started = await postMediaEndpoint({
|
|
1938
|
+
serverUrl: resolved.serverUrl,
|
|
1939
|
+
endpoint: "videoStart",
|
|
1940
|
+
body,
|
|
1941
|
+
schema: videoStartOutputSchema,
|
|
1942
|
+
label: LABEL,
|
|
1943
|
+
signal: options.abortSignal,
|
|
1944
|
+
headers: options.headers
|
|
1945
|
+
});
|
|
1946
|
+
const pollOptions = {};
|
|
1947
|
+
if (xaiOptions?.pollIntervalMs) pollOptions.pollIntervalMs = xaiOptions.pollIntervalMs;
|
|
1948
|
+
if (xaiOptions?.pollTimeoutMs) pollOptions.pollTimeoutMs = xaiOptions.pollTimeoutMs;
|
|
1949
|
+
if (options.abortSignal) pollOptions.abortSignal = options.abortSignal;
|
|
1950
|
+
if (options.headers) pollOptions.headers = options.headers;
|
|
1951
|
+
return {
|
|
1952
|
+
...await pollVideo(started.data.jobHandle, started.data.model, resolved, pollOptions, started.data.requestLogId === void 0 ? {} : { requestLogId: started.data.requestLogId }),
|
|
1953
|
+
warnings
|
|
787
1954
|
};
|
|
788
1955
|
}
|
|
789
1956
|
};
|
|
@@ -794,6 +1961,7 @@ const DEFAULT_RUN_KEY = "__qgrid_default_run__";
|
|
|
794
1961
|
const DEFAULT_STALE_RUN_TIMEOUT_MS = 1800 * 1e3;
|
|
795
1962
|
const STALE_RUN_GRACE_MS = 5e3;
|
|
796
1963
|
const OVERLAPPING_RUN_ERROR = "createQgridLogger received overlapping runs for the same telemetry key. Pass a unique metadata.qgridRunId per AI SDK call or create a fresh logger integration per call.";
|
|
1964
|
+
const qgridRunIdSchema = z.string().min(1);
|
|
797
1965
|
function isParseableJson(value) {
|
|
798
1966
|
try {
|
|
799
1967
|
JSON.parse(value ?? "");
|
|
@@ -837,7 +2005,7 @@ function createQgridLogger(config = {}) {
|
|
|
837
2005
|
const onLogError = config.onLogError ?? ((e) => console.warn(`[qgrid-logger] ${e.message}`));
|
|
838
2006
|
const runs = /* @__PURE__ */ new Map();
|
|
839
2007
|
const starting = /* @__PURE__ */ new Map();
|
|
840
|
-
const keyTtl =
|
|
2008
|
+
const keyTtl = config.staleRunTimeoutMs !== void 0 && config.staleRunTimeoutMs > 0 ? config.staleRunTimeoutMs : DEFAULT_STALE_RUN_TIMEOUT_MS;
|
|
841
2009
|
const suppressedRuns = timedKeySet();
|
|
842
2010
|
const quarantined = timedKeySet();
|
|
843
2011
|
const finalizeRun = async (runKey, result) => {
|
|
@@ -861,7 +2029,7 @@ function createQgridLogger(config = {}) {
|
|
|
861
2029
|
}).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e)))));
|
|
862
2030
|
run.pendingToolCalls = [];
|
|
863
2031
|
await Promise.allSettled(run.pendingSteps);
|
|
864
|
-
|
|
2032
|
+
const finishInput = {
|
|
865
2033
|
requestLogId: run.requestLogId,
|
|
866
2034
|
status: result.status,
|
|
867
2035
|
response: result.response,
|
|
@@ -872,16 +2040,16 @@ function createQgridLogger(config = {}) {
|
|
|
872
2040
|
totalOutputTokens: result.totalUsage?.outputTokens ?? 0,
|
|
873
2041
|
totalCacheReadTokens: result.totalUsage?.inputTokenDetails?.cacheReadTokens ?? 0,
|
|
874
2042
|
totalCacheCreationTokens: result.totalUsage?.inputTokenDetails?.cacheWriteTokens ?? 0,
|
|
875
|
-
totalDurationMs: Date.now() - run.startTime
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
2043
|
+
totalDurationMs: Date.now() - run.startTime
|
|
2044
|
+
};
|
|
2045
|
+
if (run.isStructured && result.status === "succeeded") finishInput.responseJsonOk = isParseableJson(result.response);
|
|
2046
|
+
if (result.errorMessage) finishInput.errorMessage = result.errorMessage;
|
|
2047
|
+
await finishRun(serverUrl, finishInput).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e))));
|
|
879
2048
|
};
|
|
880
2049
|
let autoRunIdCounter = 0;
|
|
881
|
-
const resolveRunKey = (
|
|
882
|
-
|
|
883
|
-
if (
|
|
884
|
-
if (typeof event.functionId === "string" && event.functionId.length > 0) return `functionId:${event.functionId}`;
|
|
2050
|
+
const resolveRunKey = (qgridRunId, functionId) => {
|
|
2051
|
+
if (qgridRunId !== void 0) return `qgridRunId:${qgridRunId}`;
|
|
2052
|
+
if (functionId) return `functionId:${functionId}`;
|
|
885
2053
|
return DEFAULT_RUN_KEY;
|
|
886
2054
|
};
|
|
887
2055
|
return {
|
|
@@ -889,7 +2057,7 @@ function createQgridLogger(config = {}) {
|
|
|
889
2057
|
integrations: [{
|
|
890
2058
|
async onStart(event) {
|
|
891
2059
|
if (!event.metadata?.qgridRunId && !event.functionId && event.metadata) event.metadata.qgridRunId = `auto-${++autoRunIdCounter}`;
|
|
892
|
-
const runKey = resolveRunKey(event);
|
|
2060
|
+
const runKey = resolveRunKey(qgridRunIdSchema.safeParse(event.metadata?.qgridRunId).data, event.functionId);
|
|
893
2061
|
if (event.model.provider === "qgrid") return;
|
|
894
2062
|
if (quarantined.has(runKey)) {
|
|
895
2063
|
onLogError(/* @__PURE__ */ new Error("createQgridLogger: telemetry key is quarantined after overlap"));
|
|
@@ -953,12 +2121,12 @@ function createQgridLogger(config = {}) {
|
|
|
953
2121
|
return;
|
|
954
2122
|
}
|
|
955
2123
|
let watchdogTimeout = DEFAULT_STALE_RUN_TIMEOUT_MS;
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
else {
|
|
959
|
-
const totalMs =
|
|
960
|
-
if (
|
|
961
|
-
}
|
|
2124
|
+
const timeout = event.timeout;
|
|
2125
|
+
if (config.staleRunTimeoutMs !== void 0) watchdogTimeout = config.staleRunTimeoutMs;
|
|
2126
|
+
else if (timeout instanceof Object) {
|
|
2127
|
+
const totalMs = timeout.totalMs;
|
|
2128
|
+
if (totalMs !== void 0 && totalMs > 0) watchdogTimeout = totalMs + STALE_RUN_GRACE_MS;
|
|
2129
|
+
} else if (timeout !== void 0 && timeout > 0) watchdogTimeout = timeout + STALE_RUN_GRACE_MS;
|
|
962
2130
|
let watchdog;
|
|
963
2131
|
if (watchdogTimeout > 0) {
|
|
964
2132
|
watchdog = setTimeout(() => {
|
|
@@ -1004,7 +2172,7 @@ function createQgridLogger(config = {}) {
|
|
|
1004
2172
|
},
|
|
1005
2173
|
onToolCallFinish(event) {
|
|
1006
2174
|
if (event.model?.provider === "qgrid") return;
|
|
1007
|
-
const runKey = resolveRunKey(event);
|
|
2175
|
+
const runKey = resolveRunKey(qgridRunIdSchema.safeParse(event.metadata?.qgridRunId).data, event.functionId);
|
|
1008
2176
|
if (suppressedRuns.has(runKey) || quarantined.has(runKey)) return;
|
|
1009
2177
|
const run = runs.get(runKey);
|
|
1010
2178
|
if (!run) return;
|
|
@@ -1012,7 +2180,7 @@ function createQgridLogger(config = {}) {
|
|
|
1012
2180
|
},
|
|
1013
2181
|
onStepFinish(event) {
|
|
1014
2182
|
if (event.model?.provider === "qgrid") return;
|
|
1015
|
-
const runKey = resolveRunKey(event);
|
|
2183
|
+
const runKey = resolveRunKey(qgridRunIdSchema.safeParse(event.metadata?.qgridRunId).data, event.functionId);
|
|
1016
2184
|
if (suppressedRuns.has(runKey) || quarantined.has(runKey)) return;
|
|
1017
2185
|
const run = runs.get(runKey);
|
|
1018
2186
|
if (!run) return;
|
|
@@ -1034,9 +2202,9 @@ function createQgridLogger(config = {}) {
|
|
|
1034
2202
|
toolArgs: pending.toolArgs,
|
|
1035
2203
|
modelName: pending.modelName,
|
|
1036
2204
|
requestedModelName: pending.requestedModelName,
|
|
1037
|
-
toolResult: tr && "output" in tr ?
|
|
2205
|
+
toolResult: tr && "output" in tr ? loggedPayloadText.parse(tr.output) : void 0,
|
|
1038
2206
|
toolDurationMs: run.toolDurations.get(pending.toolCallId),
|
|
1039
|
-
error: te && "error" in te ?
|
|
2207
|
+
error: te && "error" in te ? loggedPayloadText.parse(te.error) : void 0
|
|
1040
2208
|
}).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e)))));
|
|
1041
2209
|
run.toolDurations.delete(pending.toolCallId);
|
|
1042
2210
|
} else remainingPending.push(pending);
|
|
@@ -1053,7 +2221,7 @@ function createQgridLogger(config = {}) {
|
|
|
1053
2221
|
cacheReadTokens: usage.inputTokenDetails?.cacheReadTokens ?? 0,
|
|
1054
2222
|
cacheCreationTokens: usage.inputTokenDetails?.cacheWriteTokens ?? 0,
|
|
1055
2223
|
finishReason,
|
|
1056
|
-
reasoningText:
|
|
2224
|
+
reasoningText: reasoningText !== void 0 && reasoningText.length > 0 ? reasoningText : void 0,
|
|
1057
2225
|
reasoningTokens: usage.outputTokenDetails?.reasoningTokens
|
|
1058
2226
|
}).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e)))));
|
|
1059
2227
|
const toolCalls = content.filter((p) => p.type === "tool-call");
|
|
@@ -1068,12 +2236,12 @@ function createQgridLogger(config = {}) {
|
|
|
1068
2236
|
toolCallIndex: i,
|
|
1069
2237
|
toolCallId: tc.toolCallId,
|
|
1070
2238
|
toolName: tc.toolName,
|
|
1071
|
-
toolArgs:
|
|
2239
|
+
toolArgs: loggedPayloadText.parse(tc.input),
|
|
1072
2240
|
modelName,
|
|
1073
2241
|
requestedModelName,
|
|
1074
|
-
toolResult: tr && "output" in tr ?
|
|
2242
|
+
toolResult: tr && "output" in tr ? loggedPayloadText.parse(tr.output) : void 0,
|
|
1075
2243
|
toolDurationMs: run.toolDurations.get(tc.toolCallId),
|
|
1076
|
-
error: te && "error" in te ?
|
|
2244
|
+
error: te && "error" in te ? loggedPayloadText.parse(te.error) : void 0
|
|
1077
2245
|
}).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e)))));
|
|
1078
2246
|
run.toolDurations.delete(tc.toolCallId);
|
|
1079
2247
|
} else run.pendingToolCalls.push({
|
|
@@ -1081,7 +2249,7 @@ function createQgridLogger(config = {}) {
|
|
|
1081
2249
|
toolCallIndex: i,
|
|
1082
2250
|
toolCallId: tc.toolCallId,
|
|
1083
2251
|
toolName: tc.toolName,
|
|
1084
|
-
toolArgs:
|
|
2252
|
+
toolArgs: loggedPayloadText.parse(tc.input),
|
|
1085
2253
|
modelName,
|
|
1086
2254
|
requestedModelName
|
|
1087
2255
|
});
|
|
@@ -1089,7 +2257,7 @@ function createQgridLogger(config = {}) {
|
|
|
1089
2257
|
},
|
|
1090
2258
|
async onFinish(event) {
|
|
1091
2259
|
if (event.model?.provider === "qgrid") return;
|
|
1092
|
-
const runKey = resolveRunKey(event);
|
|
2260
|
+
const runKey = resolveRunKey(qgridRunIdSchema.safeParse(event.metadata?.qgridRunId).data, event.functionId);
|
|
1093
2261
|
if (suppressedRuns.has(runKey)) {
|
|
1094
2262
|
suppressedRuns.remove(runKey);
|
|
1095
2263
|
return;
|
|
@@ -1100,14 +2268,15 @@ function createQgridLogger(config = {}) {
|
|
|
1100
2268
|
const status = event.finishReason === "error" ? "error" : "succeeded";
|
|
1101
2269
|
const requestedModelName = fullModelName(event.model?.provider, event.model?.modelId) ?? run.requestedModelName;
|
|
1102
2270
|
const modelName = fullModelName(event.model?.provider, event.response?.modelId);
|
|
1103
|
-
|
|
2271
|
+
const result = {
|
|
1104
2272
|
status,
|
|
1105
2273
|
response: event.text,
|
|
1106
2274
|
totalUsage: event.totalUsage,
|
|
1107
2275
|
modelName,
|
|
1108
|
-
requestedModelName
|
|
1109
|
-
|
|
1110
|
-
|
|
2276
|
+
requestedModelName
|
|
2277
|
+
};
|
|
2278
|
+
if (status === "error" && "error" in event) result.errorMessage = getErrorMessage(event.error);
|
|
2279
|
+
await finalizeRun(runKey, result);
|
|
1111
2280
|
}
|
|
1112
2281
|
}]
|
|
1113
2282
|
};
|
|
@@ -1117,12 +2286,175 @@ function createQgridLogger(config = {}) {
|
|
|
1117
2286
|
const threadCoordStore = /* @__PURE__ */ new Map();
|
|
1118
2287
|
const CACHE_AFFINITY_NAMESPACE = "qgrid-cache-affinity-v1";
|
|
1119
2288
|
const MAX_THREAD_COORDS = 1e3;
|
|
2289
|
+
function isGrokModel(modelId) {
|
|
2290
|
+
return modelId.startsWith("grok/");
|
|
2291
|
+
}
|
|
2292
|
+
const IMAGE_SIGNATURES = [
|
|
2293
|
+
{
|
|
2294
|
+
mediaType: "image/png",
|
|
2295
|
+
bytes: [
|
|
2296
|
+
137,
|
|
2297
|
+
80,
|
|
2298
|
+
78,
|
|
2299
|
+
71
|
|
2300
|
+
]
|
|
2301
|
+
},
|
|
2302
|
+
{
|
|
2303
|
+
mediaType: "image/jpeg",
|
|
2304
|
+
bytes: [
|
|
2305
|
+
255,
|
|
2306
|
+
216,
|
|
2307
|
+
255
|
|
2308
|
+
]
|
|
2309
|
+
},
|
|
2310
|
+
{
|
|
2311
|
+
mediaType: "image/gif",
|
|
2312
|
+
bytes: [
|
|
2313
|
+
71,
|
|
2314
|
+
73,
|
|
2315
|
+
70,
|
|
2316
|
+
56
|
|
2317
|
+
]
|
|
2318
|
+
},
|
|
2319
|
+
{
|
|
2320
|
+
mediaType: "image/webp",
|
|
2321
|
+
bytes: [
|
|
2322
|
+
82,
|
|
2323
|
+
73,
|
|
2324
|
+
70,
|
|
2325
|
+
70
|
|
2326
|
+
]
|
|
2327
|
+
}
|
|
2328
|
+
];
|
|
2329
|
+
function sniffImageMediaType(bytes) {
|
|
2330
|
+
return IMAGE_SIGNATURES.find((signature) => signature.bytes.every((byte, index) => bytes[index] === byte))?.mediaType;
|
|
2331
|
+
}
|
|
2332
|
+
function grokImageUrl(part) {
|
|
2333
|
+
const mediaType = part.mediaType.toLowerCase();
|
|
2334
|
+
if (!mediaType.startsWith("image/")) return void 0;
|
|
2335
|
+
const { data } = part;
|
|
2336
|
+
if (data instanceof URL) return data.protocol === "https:" || data.protocol === "data:" ? data.href : void 0;
|
|
2337
|
+
if (data instanceof Uint8Array) {
|
|
2338
|
+
const resolved = mediaType === "image/*" ? sniffImageMediaType(data) : mediaType;
|
|
2339
|
+
return resolved ? `data:${resolved};base64,${bytesToBase64(data)}` : void 0;
|
|
2340
|
+
}
|
|
2341
|
+
if (/^(https:|data:)/i.test(data)) return data;
|
|
2342
|
+
if (mediaType === "image/*") return void 0;
|
|
2343
|
+
return `data:${mediaType};base64,${data}`;
|
|
2344
|
+
}
|
|
2345
|
+
function grokUserParts(content) {
|
|
2346
|
+
const parts = [];
|
|
2347
|
+
for (const part of content) if (part.type === "text") parts.push({
|
|
2348
|
+
type: "text",
|
|
2349
|
+
text: part.text
|
|
2350
|
+
});
|
|
2351
|
+
else if (part.type === "file") {
|
|
2352
|
+
const url = grokImageUrl(part);
|
|
2353
|
+
if (url !== void 0) parts.push({
|
|
2354
|
+
type: "image",
|
|
2355
|
+
url
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
return parts;
|
|
2359
|
+
}
|
|
2360
|
+
function preparePrompt(modelId, messages) {
|
|
2361
|
+
if (!isGrokModel(modelId)) {
|
|
2362
|
+
const { imageUrls, ...prepared } = extractPromptAndHistory(messages, { includeImages: modelId.startsWith("openai/") });
|
|
2363
|
+
assertImageInputFitsJsonTransport(imageUrls);
|
|
2364
|
+
return prepared;
|
|
2365
|
+
}
|
|
2366
|
+
const extracted = extractPromptAndHistory(messages);
|
|
2367
|
+
const userMessages = messages.filter((message) => message.role === "user");
|
|
2368
|
+
const lastMessage = messages.filter((message) => message.role !== "system").at(-1);
|
|
2369
|
+
const promptMessage = lastMessage?.role === "user" ? lastMessage : void 0;
|
|
2370
|
+
const historyUserMessages = promptMessage ? userMessages.slice(0, -1) : userMessages;
|
|
2371
|
+
let keptImageCount = 0;
|
|
2372
|
+
const imageUrls = [];
|
|
2373
|
+
const countImages = (parts) => {
|
|
2374
|
+
for (const part of parts) if (part.type === "image") {
|
|
2375
|
+
keptImageCount++;
|
|
2376
|
+
imageUrls.push(part.url);
|
|
2377
|
+
}
|
|
2378
|
+
};
|
|
2379
|
+
let historyUserIndex = 0;
|
|
2380
|
+
const history = extracted.history.map((item) => {
|
|
2381
|
+
if (!isUserHistoryMessage(item)) return item;
|
|
2382
|
+
const source = historyUserMessages[historyUserIndex++];
|
|
2383
|
+
if (!source) return item;
|
|
2384
|
+
const parts = grokUserParts(source.content);
|
|
2385
|
+
if (!parts.some((part) => part.type === "image")) return item;
|
|
2386
|
+
countImages(parts);
|
|
2387
|
+
return {
|
|
2388
|
+
type: "message",
|
|
2389
|
+
role: "user",
|
|
2390
|
+
content: parts.map((part) => part.type === "text" ? {
|
|
2391
|
+
type: "input_text",
|
|
2392
|
+
text: part.text
|
|
2393
|
+
} : {
|
|
2394
|
+
type: "input_image",
|
|
2395
|
+
image_url: part.url
|
|
2396
|
+
})
|
|
2397
|
+
};
|
|
2398
|
+
});
|
|
2399
|
+
let input;
|
|
2400
|
+
if (promptMessage) {
|
|
2401
|
+
const parts = grokUserParts(promptMessage.content);
|
|
2402
|
+
if (parts.some((part) => part.type === "image")) {
|
|
2403
|
+
countImages(parts);
|
|
2404
|
+
input = parts.map((part) => part.type === "text" ? {
|
|
2405
|
+
type: "text",
|
|
2406
|
+
text: part.text,
|
|
2407
|
+
text_elements: []
|
|
2408
|
+
} : {
|
|
2409
|
+
type: "image",
|
|
2410
|
+
url: part.url
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
assertImageInputFitsJsonTransport(imageUrls);
|
|
2415
|
+
const prepared = {
|
|
2416
|
+
prompt: extracted.prompt,
|
|
2417
|
+
system: extracted.system,
|
|
2418
|
+
history,
|
|
2419
|
+
droppedImageCount: extracted.droppedImageCount - keptImageCount
|
|
2420
|
+
};
|
|
2421
|
+
if (input) prepared.input = input;
|
|
2422
|
+
return prepared;
|
|
2423
|
+
}
|
|
2424
|
+
function isUserHistoryMessage(item) {
|
|
2425
|
+
return item.type === "message" && item.role === "user";
|
|
2426
|
+
}
|
|
2427
|
+
const prepareStreamOutputSchema = z.object({ streamId: z.string() });
|
|
2428
|
+
const streamDeltaSchema = z.object({ text: z.string() });
|
|
2429
|
+
const streamErrorSchema = z.object({ message: z.string() });
|
|
2430
|
+
function resolveXaiOptions(modelId, options) {
|
|
2431
|
+
const raw = options.providerOptions?.xai;
|
|
2432
|
+
if (!isGrokModel(modelId)) return raw === void 0 ? { warnings: [] } : { warnings: [{
|
|
2433
|
+
type: "unsupported",
|
|
2434
|
+
feature: "providerOptions.xai",
|
|
2435
|
+
details: `providerOptions.xai applies only to grok/ models and was ignored for ${modelId}.`
|
|
2436
|
+
}] };
|
|
2437
|
+
const mapped = mapXaiProviderOptions(raw, modelId);
|
|
2438
|
+
const { effort, promptCacheKey } = mapped.request;
|
|
2439
|
+
const resolved = { warnings: mapped.warnings.map((warning) => ({
|
|
2440
|
+
type: "unsupported",
|
|
2441
|
+
feature: `providerOptions.xai.${warning.key}`,
|
|
2442
|
+
details: warning.reason
|
|
2443
|
+
})) };
|
|
2444
|
+
if (effort !== void 0) resolved.effort = effort;
|
|
2445
|
+
if (promptCacheKey !== void 0) resolved.promptCacheKey = promptCacheKey;
|
|
2446
|
+
return resolved;
|
|
2447
|
+
}
|
|
2448
|
+
const xaiResponseMetadataSchema = z.object({ providerMetadata: z.object({ xai: z.object({
|
|
2449
|
+
costInUsdTicks: z.number().optional(),
|
|
2450
|
+
contextWindow: z.number().optional()
|
|
2451
|
+
}) }) });
|
|
1120
2452
|
function serializeObjectResponseFormat(responseFormat) {
|
|
1121
2453
|
const rawSchema = responseFormat?.type === "json" ? responseFormat.schema : void 0;
|
|
1122
2454
|
if (rawSchema === void 0) return void 0;
|
|
1123
2455
|
const schemaType = rawSchema.type;
|
|
1124
2456
|
if (schemaType !== "object") {
|
|
1125
|
-
console.warn(`[qgrid] responseFormat.schema top-level type is "${schemaType ?? "unknown"}". ${TOP_LEVEL_SCHEMA_WARNING}`);
|
|
2457
|
+
console.warn(`[qgrid] responseFormat.schema top-level type is "${Array.isArray(schemaType) ? schemaType.join(",") : schemaType ?? "unknown"}". ${TOP_LEVEL_SCHEMA_WARNING}`);
|
|
1126
2458
|
return;
|
|
1127
2459
|
}
|
|
1128
2460
|
return JSON.stringify(rawSchema);
|
|
@@ -1146,7 +2478,7 @@ function setThreadCoord(sessionKey, coord) {
|
|
|
1146
2478
|
});
|
|
1147
2479
|
if (threadCoordStore.size > MAX_THREAD_COORDS) threadCoordStore.delete(threadCoordStore.keys().next().value);
|
|
1148
2480
|
}
|
|
1149
|
-
function providerWarnings(options, droppedImageCount) {
|
|
2481
|
+
function providerWarnings(options, droppedImageCount, xaiWarnings = []) {
|
|
1150
2482
|
const warnings = [];
|
|
1151
2483
|
if (options.fallbackModels !== void 0) warnings.push({
|
|
1152
2484
|
type: "unsupported",
|
|
@@ -1156,9 +2488,9 @@ function providerWarnings(options, droppedImageCount) {
|
|
|
1156
2488
|
if (droppedImageCount > 0) warnings.push({
|
|
1157
2489
|
type: "unsupported",
|
|
1158
2490
|
feature: "image input",
|
|
1159
|
-
details: `${droppedImageCount} image part(s) were ignored. Only user-message images on openai/ models are sent; for image generation or editing, use generateImage with qgrid.image().`
|
|
2491
|
+
details: `${droppedImageCount} image part(s) were ignored. Only user-message images on openai/ and grok/ models are sent; for image generation or editing, use generateImage with qgrid.image().`
|
|
1160
2492
|
});
|
|
1161
|
-
return warnings;
|
|
2493
|
+
return [...warnings, ...xaiWarnings];
|
|
1162
2494
|
}
|
|
1163
2495
|
async function deriveCacheAffinityKey(modelId, sessionKey) {
|
|
1164
2496
|
if (!sessionKey || modelId.startsWith("anthropic/")) return void 0;
|
|
@@ -1167,22 +2499,63 @@ async function deriveCacheAffinityKey(modelId, sessionKey) {
|
|
|
1167
2499
|
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1168
2500
|
}
|
|
1169
2501
|
function qgridProviderMetadata(data) {
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
2502
|
+
const xai = xaiResponseMetadataSchema.safeParse(data);
|
|
2503
|
+
const metadata = {};
|
|
2504
|
+
if (xai.success) {
|
|
2505
|
+
const { costInUsdTicks, contextWindow } = xai.data.providerMetadata.xai;
|
|
2506
|
+
const xaiMetadata = {};
|
|
2507
|
+
if (costInUsdTicks !== void 0) xaiMetadata.costInUsdTicks = costInUsdTicks;
|
|
2508
|
+
if (contextWindow !== void 0) xaiMetadata.contextWindow = contextWindow;
|
|
2509
|
+
if (Object.keys(xaiMetadata).length > 0) metadata.xai = xaiMetadata;
|
|
2510
|
+
}
|
|
2511
|
+
const qgridMetadata = {};
|
|
2512
|
+
if (data.requestLogId !== void 0) qgridMetadata.requestLogId = data.requestLogId;
|
|
2513
|
+
qgridMetadata.model = data.model;
|
|
2514
|
+
qgridMetadata.requestedModel = data.requestedModel ?? data.model;
|
|
2515
|
+
qgridMetadata.modelFallbacks = data.modelFallbacks ?? [];
|
|
2516
|
+
qgridMetadata.tokenName = data.tokenName ?? null;
|
|
2517
|
+
qgridMetadata.durationMs = data.durationMs;
|
|
2518
|
+
qgridMetadata.costUsd = data.costUsd;
|
|
2519
|
+
qgridMetadata.costSource = data.costSource;
|
|
2520
|
+
qgridMetadata.cacheCreation5mInputTokens = data.usage.cache_creation_5m_input_tokens ?? null;
|
|
2521
|
+
qgridMetadata.cacheCreation1hInputTokens = data.usage.cache_creation_1h_input_tokens ?? null;
|
|
2522
|
+
metadata.qgrid = qgridMetadata;
|
|
2523
|
+
return metadata;
|
|
2524
|
+
}
|
|
2525
|
+
/** JSON callers often pass null for "unset"; these options treated null as absent before. */
|
|
2526
|
+
function nullAsUnset(schema) {
|
|
2527
|
+
return schema.nullish().transform((value) => value ?? void 0);
|
|
1182
2528
|
}
|
|
2529
|
+
const qgridProviderOptionsSchema = z.looseObject({
|
|
2530
|
+
thinking: z.boolean().optional(),
|
|
2531
|
+
tokenName: z.string().optional(),
|
|
2532
|
+
logger: nullAsUnset(z.boolean()),
|
|
2533
|
+
fallbackModels: nullAsUnset(z.array(z.string())),
|
|
2534
|
+
sessionKey: nullAsUnset(z.string()),
|
|
2535
|
+
effort: nullAsUnset(z.string()),
|
|
2536
|
+
verbosity: nullAsUnset(z.enum([
|
|
2537
|
+
"low",
|
|
2538
|
+
"medium",
|
|
2539
|
+
"high"
|
|
2540
|
+
])),
|
|
2541
|
+
reasoningSummary: nullAsUnset(z.enum([
|
|
2542
|
+
"auto",
|
|
2543
|
+
"concise",
|
|
2544
|
+
"detailed",
|
|
2545
|
+
"none"
|
|
2546
|
+
])),
|
|
2547
|
+
serviceTier: nullAsUnset(z.string()),
|
|
2548
|
+
timeoutMs: z.number().optional()
|
|
2549
|
+
});
|
|
1183
2550
|
function getQgridProviderOptions(options) {
|
|
1184
|
-
const
|
|
1185
|
-
if ("imageGeneration" in
|
|
2551
|
+
const raw = options.providerOptions?.qgrid ?? {};
|
|
2552
|
+
if ("imageGeneration" in raw || "imageGenerationOptions" in raw) throw new Error("qgrid: imageGeneration and imageGenerationOptions were removed from language-model options. Use generateImage with qgrid.image().");
|
|
2553
|
+
const parsed = qgridProviderOptionsSchema.safeParse(raw);
|
|
2554
|
+
if (!parsed.success) {
|
|
2555
|
+
const issues = parsed.error.issues.map((issue) => `${issue.path.map(String).join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
2556
|
+
throw new Error(`qgrid: invalid providerOptions.qgrid: ${issues}`);
|
|
2557
|
+
}
|
|
2558
|
+
const qgridOptions = parsed.data;
|
|
1186
2559
|
if (qgridOptions.tokenName !== void 0 && qgridOptions.tokenName.trim() === "") throw new Error("qgrid: tokenName must not be empty");
|
|
1187
2560
|
return qgridOptions;
|
|
1188
2561
|
}
|
|
@@ -1206,7 +2579,7 @@ function toAiSdkUsage(usage) {
|
|
|
1206
2579
|
}
|
|
1207
2580
|
function qgrid(modelId, config) {
|
|
1208
2581
|
const serverUrl = config?.serverUrl ?? process.env.QGRID_URL ?? "http://localhost:44900";
|
|
1209
|
-
const effort = config?.defaultEffort ?? "low";
|
|
2582
|
+
const effort = config?.defaultEffort ?? (isGrokModel(modelId) ? void 0 : "low");
|
|
1210
2583
|
const projectName = config?.projectName ?? process.env.QGRID_PROJECT_NAME;
|
|
1211
2584
|
const clientRuns = /* @__PURE__ */ new Map();
|
|
1212
2585
|
let anonymousClientRunId = 0;
|
|
@@ -1250,17 +2623,16 @@ function qgrid(modelId, config) {
|
|
|
1250
2623
|
const tools = options.tools?.filter((t) => t.type === "function");
|
|
1251
2624
|
const hasTools = tools && tools.length > 0;
|
|
1252
2625
|
const qgridOptions = getQgridProviderOptions(options);
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1255
|
-
const effectiveEffort = qgridOptions.thinking === false ? void 0 : qgridOptions.effort ?? effort;
|
|
2626
|
+
const xaiOptions = resolveXaiOptions(modelId, options);
|
|
2627
|
+
const { prompt, system, history, input, droppedImageCount } = preparePrompt(modelId, options.prompt);
|
|
2628
|
+
const effectiveEffort = qgridOptions.thinking === false ? void 0 : qgridOptions.effort ?? xaiOptions.effort ?? effort;
|
|
1256
2629
|
const verbosity = qgridOptions.verbosity;
|
|
1257
2630
|
const reasoningSummary = qgridOptions.reasoningSummary;
|
|
1258
2631
|
const serviceTier = qgridOptions.serviceTier;
|
|
1259
2632
|
const timeoutMs = qgridOptions.timeoutMs;
|
|
1260
2633
|
const logger = qgridOptions.logger;
|
|
1261
2634
|
const tokenName = qgridOptions.tokenName;
|
|
1262
|
-
const
|
|
1263
|
-
const cacheAffinityKey = await deriveCacheAffinityKey(modelId, sessionKey);
|
|
2635
|
+
const cacheAffinityKey = await deriveCacheAffinityKey(modelId, qgridOptions.sessionKey ?? xaiOptions.promptCacheKey);
|
|
1264
2636
|
const jsonSchema = serializeObjectResponseFormat(options.responseFormat);
|
|
1265
2637
|
let runContext;
|
|
1266
2638
|
let toolResultsPayload;
|
|
@@ -1272,33 +2644,35 @@ function qgrid(modelId, config) {
|
|
|
1272
2644
|
const storedCoord = cacheAffinityKey ? getThreadCoord(cacheAffinityKey) : void 0;
|
|
1273
2645
|
if (!matchedClientRun && storedCoord) runContext = { threadCoord: storedCoord };
|
|
1274
2646
|
const transport = createQgridRequestTransport(modelId, timeoutMs);
|
|
1275
|
-
const
|
|
2647
|
+
const args = {
|
|
2648
|
+
prompt,
|
|
2649
|
+
model: modelId,
|
|
2650
|
+
system,
|
|
2651
|
+
effort: effectiveEffort,
|
|
2652
|
+
thinking: qgridOptions.thinking
|
|
2653
|
+
};
|
|
2654
|
+
if (input) args.input = input;
|
|
2655
|
+
if (verbosity) args.verbosity = verbosity;
|
|
2656
|
+
if (reasoningSummary) args.reasoningSummary = reasoningSummary;
|
|
2657
|
+
if (serviceTier) args.serviceTier = serviceTier;
|
|
2658
|
+
if (timeoutMs !== void 0) args.timeout = timeoutMs;
|
|
2659
|
+
if (hasTools) args.tools = tools.map(toQgridTool);
|
|
2660
|
+
if (jsonSchema) args.jsonSchema = jsonSchema;
|
|
2661
|
+
if (history.length > 0) args.history = JSON.stringify(history);
|
|
2662
|
+
if (projectName) args.projectName = projectName;
|
|
2663
|
+
if (cacheAffinityKey) args.cacheAffinityKey = cacheAffinityKey;
|
|
2664
|
+
if (logger === false) args.logger = false;
|
|
2665
|
+
if (tokenName) args.tokenName = tokenName;
|
|
2666
|
+
if (runContext) args.runContext = runContext;
|
|
2667
|
+
if (toolResultsPayload) args.toolResults = toolResultsPayload;
|
|
2668
|
+
const init = {
|
|
1276
2669
|
method: "POST",
|
|
1277
2670
|
headers: { "Content-Type": "application/json" },
|
|
1278
|
-
body: JSON.stringify({ args
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
effort: effectiveEffort,
|
|
1284
|
-
thinking: qgridOptions.thinking,
|
|
1285
|
-
...verbosity ? { verbosity } : {},
|
|
1286
|
-
...reasoningSummary ? { reasoningSummary } : {},
|
|
1287
|
-
...serviceTier ? { serviceTier } : {},
|
|
1288
|
-
...timeoutMs !== void 0 ? { timeout: timeoutMs } : {},
|
|
1289
|
-
...hasTools ? { tools: tools.map(toQgridTool) } : {},
|
|
1290
|
-
...jsonSchema ? { jsonSchema } : {},
|
|
1291
|
-
...history.length > 0 ? { history: JSON.stringify(history) } : {},
|
|
1292
|
-
...projectName ? { projectName } : {},
|
|
1293
|
-
...cacheAffinityKey ? { cacheAffinityKey } : {},
|
|
1294
|
-
...logger === false ? { logger: false } : {},
|
|
1295
|
-
...tokenName ? { tokenName } : {},
|
|
1296
|
-
...runContext ? { runContext } : {},
|
|
1297
|
-
...toolResultsPayload ? { toolResults: toolResultsPayload } : {}
|
|
1298
|
-
} }),
|
|
1299
|
-
signal: options.abortSignal,
|
|
1300
|
-
...transport ? { dispatcher: transport.dispatcher } : {}
|
|
1301
|
-
}, {
|
|
2671
|
+
body: JSON.stringify({ args }),
|
|
2672
|
+
signal: options.abortSignal
|
|
2673
|
+
};
|
|
2674
|
+
if (transport) init.dispatcher = transport.dispatcher;
|
|
2675
|
+
const data = await qgridFetch(`${serverUrl}/api/qgrid/query`, init, {
|
|
1302
2676
|
operation: "query",
|
|
1303
2677
|
serverUrl,
|
|
1304
2678
|
transportTimeoutMs: transport?.timeoutMs
|
|
@@ -1307,7 +2681,7 @@ function qgrid(modelId, config) {
|
|
|
1307
2681
|
const text = await res.text().catch(() => "");
|
|
1308
2682
|
throw new Error(`qgrid ${res.status}: ${text}`);
|
|
1309
2683
|
}
|
|
1310
|
-
return await res.json();
|
|
2684
|
+
return queryOutputSchema.parse(await res.json());
|
|
1311
2685
|
}).finally(() => closeQgridRequestTransport(transport));
|
|
1312
2686
|
const content = [];
|
|
1313
2687
|
let finishReason = {
|
|
@@ -1334,13 +2708,13 @@ function qgrid(modelId, config) {
|
|
|
1334
2708
|
text: data.text
|
|
1335
2709
|
});
|
|
1336
2710
|
if (matchedClientRun) clientRuns.delete(matchedClientRun.key);
|
|
1337
|
-
if (finishReason.unified === "tool-calls") rememberClientRun(data.runContext, content.
|
|
2711
|
+
if (finishReason.unified === "tool-calls") rememberClientRun(data.runContext, content.flatMap((c) => c.type === "tool-call" ? [c.toolCallId] : []));
|
|
1338
2712
|
if (cacheAffinityKey && data.runContext?.threadCoord) setThreadCoord(cacheAffinityKey, data.runContext.threadCoord);
|
|
1339
2713
|
return {
|
|
1340
2714
|
content,
|
|
1341
2715
|
finishReason,
|
|
1342
2716
|
usage: toAiSdkUsage(data.usage),
|
|
1343
|
-
warnings: providerWarnings(qgridOptions, droppedImageCount),
|
|
2717
|
+
warnings: providerWarnings(qgridOptions, droppedImageCount, xaiOptions.warnings),
|
|
1344
2718
|
providerMetadata: qgridProviderMetadata(data),
|
|
1345
2719
|
response: { modelId: data.model }
|
|
1346
2720
|
};
|
|
@@ -1349,17 +2723,16 @@ function qgrid(modelId, config) {
|
|
|
1349
2723
|
const tools = options.tools?.filter((t) => t.type === "function");
|
|
1350
2724
|
const hasTools = tools && tools.length > 0;
|
|
1351
2725
|
const qgridOptions = getQgridProviderOptions(options);
|
|
1352
|
-
const
|
|
2726
|
+
const xaiOptions = resolveXaiOptions(modelId, options);
|
|
2727
|
+
const effectiveEffort = qgridOptions.thinking === false ? void 0 : qgridOptions.effort ?? xaiOptions.effort ?? effort;
|
|
1353
2728
|
const verbosity = qgridOptions.verbosity;
|
|
1354
2729
|
const reasoningSummary = qgridOptions.reasoningSummary;
|
|
1355
2730
|
const serviceTier = qgridOptions.serviceTier;
|
|
1356
2731
|
const timeoutMs = qgridOptions.timeoutMs;
|
|
1357
2732
|
const logger = qgridOptions.logger;
|
|
1358
2733
|
const tokenName = qgridOptions.tokenName;
|
|
1359
|
-
const { prompt, system, history, input,
|
|
1360
|
-
|
|
1361
|
-
const sessionKey = qgridOptions.sessionKey;
|
|
1362
|
-
const cacheAffinityKey = await deriveCacheAffinityKey(modelId, sessionKey);
|
|
2734
|
+
const { prompt, system, history, input, droppedImageCount } = preparePrompt(modelId, options.prompt);
|
|
2735
|
+
const cacheAffinityKey = await deriveCacheAffinityKey(modelId, qgridOptions.sessionKey ?? xaiOptions.promptCacheKey);
|
|
1363
2736
|
const jsonSchema = serializeObjectResponseFormat(options.responseFormat);
|
|
1364
2737
|
let runContext;
|
|
1365
2738
|
let toolResultsPayload;
|
|
@@ -1372,37 +2745,38 @@ function qgrid(modelId, config) {
|
|
|
1372
2745
|
if (!matchedClientRun && storedCoord) runContext = { threadCoord: storedCoord };
|
|
1373
2746
|
const abortController = new AbortController();
|
|
1374
2747
|
const signal = options.abortSignal ? AbortSignal.any([options.abortSignal, abortController.signal]) : abortController.signal;
|
|
2748
|
+
const args = {
|
|
2749
|
+
prompt,
|
|
2750
|
+
model: modelId,
|
|
2751
|
+
system,
|
|
2752
|
+
effort: effectiveEffort,
|
|
2753
|
+
thinking: qgridOptions.thinking
|
|
2754
|
+
};
|
|
2755
|
+
if (input) args.input = input;
|
|
2756
|
+
if (verbosity) args.verbosity = verbosity;
|
|
2757
|
+
if (reasoningSummary) args.reasoningSummary = reasoningSummary;
|
|
2758
|
+
if (serviceTier) args.serviceTier = serviceTier;
|
|
2759
|
+
if (timeoutMs !== void 0) args.timeout = timeoutMs;
|
|
2760
|
+
if (hasTools) args.tools = tools.map(toQgridTool);
|
|
2761
|
+
if (jsonSchema) args.jsonSchema = jsonSchema;
|
|
2762
|
+
if (history.length > 0) args.history = JSON.stringify(history);
|
|
2763
|
+
if (projectName) args.projectName = projectName;
|
|
2764
|
+
if (cacheAffinityKey) args.cacheAffinityKey = cacheAffinityKey;
|
|
2765
|
+
if (logger === false) args.logger = false;
|
|
2766
|
+
if (tokenName) args.tokenName = tokenName;
|
|
2767
|
+
if (runContext) args.runContext = runContext;
|
|
2768
|
+
if (toolResultsPayload) args.toolResults = toolResultsPayload;
|
|
1375
2769
|
const prepRes = await fetch(`${serverUrl}/api/qgrid/prepareStream`, {
|
|
1376
2770
|
method: "POST",
|
|
1377
2771
|
headers: { "Content-Type": "application/json" },
|
|
1378
|
-
body: JSON.stringify({ args
|
|
1379
|
-
prompt,
|
|
1380
|
-
...input ? { input } : {},
|
|
1381
|
-
model: modelId,
|
|
1382
|
-
system,
|
|
1383
|
-
effort: effectiveEffort,
|
|
1384
|
-
thinking: qgridOptions.thinking,
|
|
1385
|
-
...verbosity ? { verbosity } : {},
|
|
1386
|
-
...reasoningSummary ? { reasoningSummary } : {},
|
|
1387
|
-
...serviceTier ? { serviceTier } : {},
|
|
1388
|
-
...timeoutMs !== void 0 ? { timeout: timeoutMs } : {},
|
|
1389
|
-
...hasTools ? { tools: tools.map(toQgridTool) } : {},
|
|
1390
|
-
...jsonSchema ? { jsonSchema } : {},
|
|
1391
|
-
...history.length > 0 ? { history: JSON.stringify(history) } : {},
|
|
1392
|
-
...projectName ? { projectName } : {},
|
|
1393
|
-
...cacheAffinityKey ? { cacheAffinityKey } : {},
|
|
1394
|
-
...logger === false ? { logger: false } : {},
|
|
1395
|
-
...tokenName ? { tokenName } : {},
|
|
1396
|
-
...runContext ? { runContext } : {},
|
|
1397
|
-
...toolResultsPayload ? { toolResults: toolResultsPayload } : {}
|
|
1398
|
-
} }),
|
|
2772
|
+
body: JSON.stringify({ args }),
|
|
1399
2773
|
signal
|
|
1400
2774
|
});
|
|
1401
2775
|
if (!prepRes.ok) {
|
|
1402
2776
|
const text = await prepRes.text().catch(() => "");
|
|
1403
2777
|
throw new Error(`qgrid prepareStream ${prepRes.status}: ${text}`);
|
|
1404
2778
|
}
|
|
1405
|
-
const { streamId } = await prepRes.json();
|
|
2779
|
+
const { streamId } = prepareStreamOutputSchema.parse(await prepRes.json());
|
|
1406
2780
|
const streamRes = await fetch(`${serverUrl}/api/qgrid/queryStream?streamId=${streamId}`, { signal });
|
|
1407
2781
|
if (!streamRes.ok || !streamRes.body) {
|
|
1408
2782
|
const text = await streamRes.text().catch(() => "");
|
|
@@ -1428,14 +2802,14 @@ function qgrid(modelId, config) {
|
|
|
1428
2802
|
try {
|
|
1429
2803
|
controller.enqueue({
|
|
1430
2804
|
type: "stream-start",
|
|
1431
|
-
warnings: providerWarnings(qgridOptions, droppedImageCount)
|
|
2805
|
+
warnings: providerWarnings(qgridOptions, droppedImageCount, xaiOptions.warnings)
|
|
1432
2806
|
});
|
|
1433
2807
|
let streamCompleted = false;
|
|
1434
2808
|
for await (const event of parseSSE(streamRes.body, signal)) {
|
|
1435
2809
|
if (cancelled) return;
|
|
1436
2810
|
signal.throwIfAborted();
|
|
1437
2811
|
if (event.type === "delta") {
|
|
1438
|
-
const raw = event.data.text;
|
|
2812
|
+
const raw = streamDeltaSchema.parse(event.data).text;
|
|
1439
2813
|
const text = envelopeParser ? envelopeParser.push(raw) : raw;
|
|
1440
2814
|
if (text !== "") {
|
|
1441
2815
|
if (!textStarted) {
|
|
@@ -1460,7 +2834,7 @@ function qgrid(modelId, config) {
|
|
|
1460
2834
|
});
|
|
1461
2835
|
textStarted = false;
|
|
1462
2836
|
}
|
|
1463
|
-
const done = event.data;
|
|
2837
|
+
const done = queryOutputSchema.parse(event.data);
|
|
1464
2838
|
if (matchedClientRun) clientRuns.delete(matchedClientRun.key);
|
|
1465
2839
|
if (done.finishReason === "tool-calls") rememberClientRun(done.runContext, (done.content ?? []).filter((c) => c.type === "tool-call").map((c) => c.toolCallId));
|
|
1466
2840
|
if (cacheAffinityKey && done.runContext?.threadCoord) setThreadCoord(cacheAffinityKey, done.runContext.threadCoord);
|
|
@@ -1524,7 +2898,8 @@ function qgrid(modelId, config) {
|
|
|
1524
2898
|
return;
|
|
1525
2899
|
} else if (event.type === "error") {
|
|
1526
2900
|
streamCompleted = true;
|
|
1527
|
-
|
|
2901
|
+
const streamError = streamErrorSchema.safeParse(event.data);
|
|
2902
|
+
controller.error(new Error(streamError.success ? streamError.data.message : JSON.stringify(event.data)));
|
|
1528
2903
|
return;
|
|
1529
2904
|
}
|
|
1530
2905
|
}
|
|
@@ -1541,5 +2916,8 @@ function qgrid(modelId, config) {
|
|
|
1541
2916
|
};
|
|
1542
2917
|
}
|
|
1543
2918
|
qgrid.image = createQgridImage;
|
|
2919
|
+
qgrid.video = createQgridVideo;
|
|
2920
|
+
qgrid.transcription = createQgridTranscription;
|
|
2921
|
+
qgrid.speech = createQgridSpeech;
|
|
1544
2922
|
//#endregion
|
|
1545
|
-
export { createQgridLogger, qgrid as default, qgrid };
|
|
2923
|
+
export { QGRID_MAX_REQUEST_BODY_BYTES, QgridMediaHttpError, QgridVideoGenerationError, QgridVideoTimeoutError, QgridXaiOptionsError, createQgridLogger, qgrid as default, qgrid, resumeQgridVideo };
|