@cartanova/qgrid-ai-sdk 2.10.8 → 2.10.9

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 CHANGED
@@ -323,7 +323,9 @@ const { image } = await generateImage({
323
323
  정확한 최종 크기는 소비 앱의 명시적인 crop/resize 정책으로 처리합니다.
324
324
 
325
325
  `generateText`와 `streamText`는 이미지 생성 옵션이 없는 대화 모델 인터페이스입니다.
326
- 일반적인 이미지 인식도 지원하지 않아 image part는 경고와 함께 무시됩니다. 이미지 생성·편집이
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. General vision is also unsupported there: image parts are ignored with a warning. Use
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
- for (const msg of messages) if (msg.role === "system") system = extractPromptTextParts(msg.content).text;
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 = extractPromptTextParts(last.content);
462
+ const extracted = extractUser(last.content);
463
463
  prompt = extracted.text;
464
- droppedImageCount += extracted.droppedImageCount;
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 = extractPromptTextParts(msg.content);
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
- droppedImageCount
513
+ imageUrls,
514
+ droppedImageCount,
515
+ ...input ? { input } : {}
515
516
  };
516
517
  }
517
- function extractPromptTextParts(content) {
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") parts.push({
529
- type: "input_text",
530
- text: part.text
531
- });
532
- else if (isImagePart(part)) droppedImageCount++;
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: parts.map((part) => part.text).join("\n"),
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 generateImage, for example as WebP/JPEG under ${MAX_IMAGE_INPUT_DATA_URL_CHARS_LABEL} total base64 data-url chars.`);
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 generateImage, for example as WebP/JPEG under ${MAX_IMAGE_INPUT_DATA_URL_CHARS_LABEL} total base64 data-url chars.`);
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
- return options.fallbackModels === void 0 ? [] : [{
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
- warnDroppedImages(droppedImageCount);
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
- warnDroppedImages(droppedImageCount);
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)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartanova/qgrid-ai-sdk",
3
- "version": "2.10.8",
3
+ "version": "2.10.9",
4
4
  "description": "AI SDK LanguageModelV3 and ImageModelV3 provider for qgrid",
5
5
  "repository": {
6
6
  "type": "git",