@cartanova/qgrid-ai-sdk 2.10.8 → 2.10.10
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 +3 -1
- package/README.md +3 -1
- package/dist/index.js +85 -39
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -323,7 +323,9 @@ const { image } = await generateImage({
|
|
|
323
323
|
정확한 최종 크기는 소비 앱의 명시적인 crop/resize 정책으로 처리합니다.
|
|
324
324
|
|
|
325
325
|
`generateText`와 `streamText`는 이미지 생성 옵션이 없는 대화 모델 인터페이스입니다.
|
|
326
|
-
|
|
326
|
+
`openai/` 모델은 user 메시지의 image part를 모델에 전달하므로 이미지 인식(image→text)이 됩니다.
|
|
327
|
+
`anthropic/` 모델과 assistant 메시지의 이미지는 버려지고 `result.warnings`에 `unsupported` 경고가
|
|
328
|
+
남습니다. 이미지 생성·편집이
|
|
327
329
|
목적이면 `generateImage`의 참조 입력을 사용하세요. 대시보드 `qgrid_chat`은 내부 이미지
|
|
328
330
|
도구를 기본으로 등록합니다. 채팅 모델이 생성 여부를 판단하고 완결된 프롬프트를 작성하며,
|
|
329
331
|
편집 시 최근 대화 이미지 중 최대 5개를 선택합니다. 서버는 일반·투명 이미지 모두 구독
|
package/README.md
CHANGED
|
@@ -332,7 +332,9 @@ alpha when saving. Fully opaque, entirely empty, or invalid PNG results fail exp
|
|
|
332
332
|
not remove backgrounds or resize output. Apply any final crop/resize policy in the consuming app.
|
|
333
333
|
|
|
334
334
|
`generateText` and `streamText` use the language-model interface, which has no image-generation
|
|
335
|
-
options.
|
|
335
|
+
options. Image input (image→text) works on `openai/` models: user-message image parts are sent
|
|
336
|
+
to the model. `anthropic/` models and assistant-message images drop image parts and report an
|
|
337
|
+
`unsupported` warning in `result.warnings`. Use
|
|
336
338
|
`generateImage` references when the intended result is a generated or edited image. Dashboard
|
|
337
339
|
`qgrid_chat` registers an internal image tool by default: its chat model decides whether to
|
|
338
340
|
generate an image, writes a self-contained prompt, and selects up to five recent conversation
|
package/dist/index.js
CHANGED
|
@@ -434,42 +434,41 @@ async function* parseSSE(body, signal) {
|
|
|
434
434
|
}
|
|
435
435
|
}
|
|
436
436
|
}
|
|
437
|
-
function extractPromptAndHistory(messages) {
|
|
437
|
+
function extractPromptAndHistory(messages, options = {}) {
|
|
438
|
+
const includeImages = options.includeImages ?? false;
|
|
439
|
+
const imageUrls = [];
|
|
438
440
|
let droppedImageCount = 0;
|
|
439
441
|
let system;
|
|
440
442
|
const nonSystem = [];
|
|
441
|
-
|
|
443
|
+
const extractUser = (content) => {
|
|
444
|
+
const extracted = extractPromptParts(content, includeImages);
|
|
445
|
+
imageUrls.push(...extracted.imageUrls);
|
|
446
|
+
droppedImageCount += extracted.droppedImageCount;
|
|
447
|
+
return extracted;
|
|
448
|
+
};
|
|
449
|
+
for (const msg of messages) if (msg.role === "system") system = extractPromptParts(msg.content, false).text;
|
|
442
450
|
else nonSystem.push(msg);
|
|
443
451
|
if (nonSystem.length === 0) return {
|
|
444
452
|
prompt: "",
|
|
445
453
|
system,
|
|
446
454
|
history: [],
|
|
455
|
+
imageUrls,
|
|
447
456
|
droppedImageCount
|
|
448
457
|
};
|
|
449
|
-
if (nonSystem.length === 1 && nonSystem[0].role === "user") {
|
|
450
|
-
const extracted = extractPromptTextParts(nonSystem[0].content);
|
|
451
|
-
droppedImageCount += extracted.droppedImageCount;
|
|
452
|
-
return {
|
|
453
|
-
prompt: extracted.text,
|
|
454
|
-
system,
|
|
455
|
-
history: [],
|
|
456
|
-
droppedImageCount
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
458
|
const last = nonSystem[nonSystem.length - 1];
|
|
460
459
|
let prompt = "";
|
|
460
|
+
let input;
|
|
461
461
|
if (last.role === "user") {
|
|
462
|
-
const extracted =
|
|
462
|
+
const extracted = extractUser(last.content);
|
|
463
463
|
prompt = extracted.text;
|
|
464
|
-
|
|
464
|
+
input = inputPartsFromPromptParts(extracted.parts);
|
|
465
465
|
}
|
|
466
466
|
const historyEnd = last.role === "user" ? nonSystem.length - 1 : nonSystem.length;
|
|
467
467
|
const history = [];
|
|
468
468
|
for (let i = 0; i < historyEnd; i++) {
|
|
469
469
|
const msg = nonSystem[i];
|
|
470
470
|
if (msg.role === "user") {
|
|
471
|
-
const extracted =
|
|
472
|
-
droppedImageCount += extracted.droppedImageCount;
|
|
471
|
+
const extracted = extractUser(msg.content);
|
|
473
472
|
const content = extracted.parts;
|
|
474
473
|
history.push({
|
|
475
474
|
type: "message",
|
|
@@ -511,36 +510,78 @@ function extractPromptAndHistory(messages) {
|
|
|
511
510
|
prompt,
|
|
512
511
|
system,
|
|
513
512
|
history,
|
|
514
|
-
|
|
513
|
+
imageUrls,
|
|
514
|
+
droppedImageCount,
|
|
515
|
+
...input ? { input } : {}
|
|
515
516
|
};
|
|
516
517
|
}
|
|
517
|
-
function
|
|
518
|
+
function extractPromptParts(content, includeImages) {
|
|
518
519
|
if (typeof content === "string") return {
|
|
519
520
|
text: content,
|
|
520
521
|
parts: [{
|
|
521
522
|
type: "input_text",
|
|
522
523
|
text: content
|
|
523
524
|
}],
|
|
525
|
+
imageUrls: [],
|
|
524
526
|
droppedImageCount: 0
|
|
525
527
|
};
|
|
526
528
|
const parts = [];
|
|
529
|
+
const texts = [];
|
|
530
|
+
const imageUrls = [];
|
|
527
531
|
let droppedImageCount = 0;
|
|
528
|
-
for (const part of content) if ("text" in part && typeof part.text === "string")
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
532
|
+
for (const part of content) if ("text" in part && typeof part.text === "string") {
|
|
533
|
+
parts.push({
|
|
534
|
+
type: "input_text",
|
|
535
|
+
text: part.text
|
|
536
|
+
});
|
|
537
|
+
texts.push(part.text);
|
|
538
|
+
} else if (isImagePart(part)) {
|
|
539
|
+
const url = includeImages ? imageUrl(part) : void 0;
|
|
540
|
+
if (url) {
|
|
541
|
+
parts.push({
|
|
542
|
+
type: "input_image",
|
|
543
|
+
image_url: url
|
|
544
|
+
});
|
|
545
|
+
imageUrls.push(url);
|
|
546
|
+
} else droppedImageCount++;
|
|
547
|
+
}
|
|
533
548
|
return {
|
|
534
|
-
text:
|
|
549
|
+
text: texts.join("\n"),
|
|
535
550
|
parts,
|
|
551
|
+
imageUrls,
|
|
536
552
|
droppedImageCount
|
|
537
553
|
};
|
|
538
554
|
}
|
|
555
|
+
function inputPartsFromPromptParts(parts) {
|
|
556
|
+
if (!parts.some((part) => part.type === "input_image")) return void 0;
|
|
557
|
+
return parts.flatMap((part) => {
|
|
558
|
+
if (part.type === "input_image") return [{
|
|
559
|
+
type: "image",
|
|
560
|
+
url: part.image_url
|
|
561
|
+
}];
|
|
562
|
+
return part.text ? [{
|
|
563
|
+
type: "text",
|
|
564
|
+
text: part.text,
|
|
565
|
+
text_elements: []
|
|
566
|
+
}] : [];
|
|
567
|
+
});
|
|
568
|
+
}
|
|
539
569
|
function isImagePart(part) {
|
|
540
570
|
if (!part || typeof part !== "object") return false;
|
|
541
571
|
const record = part;
|
|
542
572
|
return record.type === "image" || record.type === "file" && typeof record.mediaType === "string" && record.mediaType.toLowerCase().startsWith("image/");
|
|
543
573
|
}
|
|
574
|
+
function imageUrl(part) {
|
|
575
|
+
const record = part;
|
|
576
|
+
const mediaType = typeof record.mediaType === "string" && record.mediaType !== "image/*" ? record.mediaType : "image/png";
|
|
577
|
+
const data = record.data ?? record.image;
|
|
578
|
+
if (data instanceof URL) return /^https?:$/i.test(data.protocol) ? data.toString() : void 0;
|
|
579
|
+
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
|
+
if (/^(https?:|data:)/i.test(data)) return data;
|
|
583
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(data) ? void 0 : `data:${mediaType};base64,${data}`;
|
|
584
|
+
}
|
|
544
585
|
function bytesToBase64(bytes) {
|
|
545
586
|
if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
|
|
546
587
|
const chunkSize = 32768;
|
|
@@ -555,9 +596,9 @@ function assertImageInputFitsJsonTransport(imageUrls) {
|
|
|
555
596
|
for (const url of imageUrls) {
|
|
556
597
|
if (!url.toLowerCase().startsWith("data:")) continue;
|
|
557
598
|
totalDataUrlChars += url.length;
|
|
558
|
-
if (url.length > MAX_IMAGE_INPUT_DATA_URL_CHARS) throw new Error(`qgrid: image input is too large for JSON transport (${url.length} chars). Compress or resize it before passing it to
|
|
599
|
+
if (url.length > MAX_IMAGE_INPUT_DATA_URL_CHARS) throw new Error(`qgrid: image input is too large for JSON transport (${url.length} chars). Compress or resize it before passing it to qgrid, for example as WebP/JPEG under ${MAX_IMAGE_INPUT_DATA_URL_CHARS_LABEL} total base64 data-url chars.`);
|
|
559
600
|
}
|
|
560
|
-
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
|
|
601
|
+
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.`);
|
|
561
602
|
}
|
|
562
603
|
function safeStringify(value) {
|
|
563
604
|
try {
|
|
@@ -1076,10 +1117,6 @@ function createQgridLogger(config = {}) {
|
|
|
1076
1117
|
const threadCoordStore = /* @__PURE__ */ new Map();
|
|
1077
1118
|
const CACHE_AFFINITY_NAMESPACE = "qgrid-cache-affinity-v1";
|
|
1078
1119
|
const MAX_THREAD_COORDS = 1e3;
|
|
1079
|
-
function warnDroppedImages(droppedImageCount) {
|
|
1080
|
-
if (droppedImageCount <= 0) return;
|
|
1081
|
-
console.warn(`[qgrid] ${droppedImageCount} image message part(s) were ignored because qgrid language models do not support image input. For image generation or editing, use generateImage with qgrid.image().`);
|
|
1082
|
-
}
|
|
1083
1120
|
function serializeObjectResponseFormat(responseFormat) {
|
|
1084
1121
|
const rawSchema = responseFormat?.type === "json" ? responseFormat.schema : void 0;
|
|
1085
1122
|
if (rawSchema === void 0) return void 0;
|
|
@@ -1109,12 +1146,19 @@ function setThreadCoord(sessionKey, coord) {
|
|
|
1109
1146
|
});
|
|
1110
1147
|
if (threadCoordStore.size > MAX_THREAD_COORDS) threadCoordStore.delete(threadCoordStore.keys().next().value);
|
|
1111
1148
|
}
|
|
1112
|
-
function providerWarnings(options) {
|
|
1113
|
-
|
|
1149
|
+
function providerWarnings(options, droppedImageCount) {
|
|
1150
|
+
const warnings = [];
|
|
1151
|
+
if (options.fallbackModels !== void 0) warnings.push({
|
|
1114
1152
|
type: "unsupported",
|
|
1115
1153
|
feature: "providerOptions.qgrid.fallbackModels",
|
|
1116
1154
|
details: "fallbackModels is deprecated and ignored; qgrid does not implement model fallback routing."
|
|
1117
|
-
}
|
|
1155
|
+
});
|
|
1156
|
+
if (droppedImageCount > 0) warnings.push({
|
|
1157
|
+
type: "unsupported",
|
|
1158
|
+
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().`
|
|
1160
|
+
});
|
|
1161
|
+
return warnings;
|
|
1118
1162
|
}
|
|
1119
1163
|
async function deriveCacheAffinityKey(modelId, sessionKey) {
|
|
1120
1164
|
if (!sessionKey || modelId.startsWith("anthropic/")) return void 0;
|
|
@@ -1206,8 +1250,8 @@ function qgrid(modelId, config) {
|
|
|
1206
1250
|
const tools = options.tools?.filter((t) => t.type === "function");
|
|
1207
1251
|
const hasTools = tools && tools.length > 0;
|
|
1208
1252
|
const qgridOptions = getQgridProviderOptions(options);
|
|
1209
|
-
const { prompt, system, history, droppedImageCount } = extractPromptAndHistory(options.prompt);
|
|
1210
|
-
|
|
1253
|
+
const { prompt, system, history, input, imageUrls, droppedImageCount } = extractPromptAndHistory(options.prompt, { includeImages: modelId.startsWith("openai/") });
|
|
1254
|
+
assertImageInputFitsJsonTransport(imageUrls);
|
|
1211
1255
|
const effectiveEffort = qgridOptions.thinking === false ? void 0 : qgridOptions.effort ?? effort;
|
|
1212
1256
|
const verbosity = qgridOptions.verbosity;
|
|
1213
1257
|
const reasoningSummary = qgridOptions.reasoningSummary;
|
|
@@ -1233,6 +1277,7 @@ function qgrid(modelId, config) {
|
|
|
1233
1277
|
headers: { "Content-Type": "application/json" },
|
|
1234
1278
|
body: JSON.stringify({ args: {
|
|
1235
1279
|
prompt,
|
|
1280
|
+
...input ? { input } : {},
|
|
1236
1281
|
model: modelId,
|
|
1237
1282
|
system,
|
|
1238
1283
|
effort: effectiveEffort,
|
|
@@ -1295,7 +1340,7 @@ function qgrid(modelId, config) {
|
|
|
1295
1340
|
content,
|
|
1296
1341
|
finishReason,
|
|
1297
1342
|
usage: toAiSdkUsage(data.usage),
|
|
1298
|
-
warnings: providerWarnings(qgridOptions),
|
|
1343
|
+
warnings: providerWarnings(qgridOptions, droppedImageCount),
|
|
1299
1344
|
providerMetadata: qgridProviderMetadata(data),
|
|
1300
1345
|
response: { modelId: data.model }
|
|
1301
1346
|
};
|
|
@@ -1311,8 +1356,8 @@ function qgrid(modelId, config) {
|
|
|
1311
1356
|
const timeoutMs = qgridOptions.timeoutMs;
|
|
1312
1357
|
const logger = qgridOptions.logger;
|
|
1313
1358
|
const tokenName = qgridOptions.tokenName;
|
|
1314
|
-
const { prompt, system, history, droppedImageCount } = extractPromptAndHistory(options.prompt);
|
|
1315
|
-
|
|
1359
|
+
const { prompt, system, history, input, imageUrls, droppedImageCount } = extractPromptAndHistory(options.prompt, { includeImages: modelId.startsWith("openai/") });
|
|
1360
|
+
assertImageInputFitsJsonTransport(imageUrls);
|
|
1316
1361
|
const sessionKey = qgridOptions.sessionKey;
|
|
1317
1362
|
const cacheAffinityKey = await deriveCacheAffinityKey(modelId, sessionKey);
|
|
1318
1363
|
const jsonSchema = serializeObjectResponseFormat(options.responseFormat);
|
|
@@ -1332,6 +1377,7 @@ function qgrid(modelId, config) {
|
|
|
1332
1377
|
headers: { "Content-Type": "application/json" },
|
|
1333
1378
|
body: JSON.stringify({ args: {
|
|
1334
1379
|
prompt,
|
|
1380
|
+
...input ? { input } : {},
|
|
1335
1381
|
model: modelId,
|
|
1336
1382
|
system,
|
|
1337
1383
|
effort: effectiveEffort,
|
|
@@ -1382,7 +1428,7 @@ function qgrid(modelId, config) {
|
|
|
1382
1428
|
try {
|
|
1383
1429
|
controller.enqueue({
|
|
1384
1430
|
type: "stream-start",
|
|
1385
|
-
warnings: providerWarnings(qgridOptions)
|
|
1431
|
+
warnings: providerWarnings(qgridOptions, droppedImageCount)
|
|
1386
1432
|
});
|
|
1387
1433
|
let streamCompleted = false;
|
|
1388
1434
|
for await (const event of parseSSE(streamRes.body, signal)) {
|