@cartanova/qgrid-ai-sdk 2.3.9 → 2.4.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 CHANGED
@@ -203,8 +203,27 @@ const result = await generateText({
203
203
  const image = result.files[0]; // mediaType: "image/png", base64
204
204
  ```
205
205
 
206
+ 레퍼런스 이미지는 일반 AI SDK multimodal message part로 전달할 수 있습니다:
207
+
208
+ ```typescript
209
+ const result = await generateText({
210
+ model: qgrid("openai/gpt-5.4"),
211
+ messages: [
212
+ {
213
+ role: "user",
214
+ content: [
215
+ { type: "text", text: "이 이미지를 스타일 레퍼런스로 사용해서 포스터를 만들어줘" },
216
+ { type: "file", mediaType: "image/png", data: referenceImageBase64 },
217
+ ],
218
+ },
219
+ ],
220
+ providerOptions: { qgrid: { imageGeneration: true } },
221
+ });
222
+ ```
223
+
206
224
  - `streamText`에서는 거부됩니다 (non-stream 전용).
207
225
  - 이미지 생성 요청은 항상 cold one-shot thread로 실행되며 thread reuse 대상에서 제외됩니다.
226
+ - 레퍼런스 이미지는 JSON data URL로 전송됩니다. 큰 사진은 압축하거나 리사이즈해서 전달하세요. 과도하게 큰 base64 입력은 SDK가 거부하며, 사진에는 WebP/JPEG를 권장합니다.
208
227
  - 이미지 비용은 `gpt-image-2` 공개 단가표 기반 **추정치**로 request log의 `image_cost_usd`에 별도 기록됩니다 (codex가 정확한 이미지 tool 사용량을 노출하지 않음).
209
228
 
210
229
  ## Telemetry Logger
package/README.md CHANGED
@@ -203,8 +203,27 @@ const result = await generateText({
203
203
  const image = result.files[0]; // mediaType: "image/png", base64
204
204
  ```
205
205
 
206
+ Reference images can be passed through normal AI SDK multimodal message parts:
207
+
208
+ ```typescript
209
+ const result = await generateText({
210
+ model: qgrid("openai/gpt-5.4"),
211
+ messages: [
212
+ {
213
+ role: "user",
214
+ content: [
215
+ { type: "text", text: "Use this image as a style reference and create a poster" },
216
+ { type: "file", mediaType: "image/png", data: referenceImageBase64 },
217
+ ],
218
+ },
219
+ ],
220
+ providerOptions: { qgrid: { imageGeneration: true } },
221
+ });
222
+ ```
223
+
206
224
  - Rejected in `streamText` (non-stream only).
207
225
  - Image-generation requests always run on a cold one-shot thread and are excluded from thread reuse.
226
+ - Reference images are sent as JSON data URLs. Compress or resize large photos before passing them in; oversized base64 inputs are rejected by the SDK. WebP/JPEG is recommended for photos.
208
227
  - The image cost is an **estimate** based on the public `gpt-image-2` price table, recorded separately as `image_cost_usd` on the request log (codex does not expose exact image-tool usage).
209
228
 
210
229
  ## Telemetry Logger
package/dist/index.js CHANGED
@@ -84,7 +84,10 @@ async function* parseSSE(body) {
84
84
  }
85
85
  }
86
86
  }
87
- function extractPromptAndHistory(messages) {
87
+ function extractPromptAndHistory(messages, options = {}) {
88
+ const includeImages = options.includeImages ?? true;
89
+ const imageUrls = [];
90
+ let droppedImageCount = 0;
88
91
  let system;
89
92
  const nonSystem = [];
90
93
  for (const msg of messages) if (msg.role === "system") system = extractTextFromContent(msg.content);
@@ -92,28 +95,51 @@ function extractPromptAndHistory(messages) {
92
95
  if (nonSystem.length === 0) return {
93
96
  prompt: "",
94
97
  system,
95
- history: []
96
- };
97
- if (nonSystem.length === 1 && nonSystem[0].role === "user") return {
98
- prompt: extractTextFromContent(nonSystem[0].content),
99
- system,
100
- history: []
98
+ history: [],
99
+ imageUrls,
100
+ droppedImageCount
101
101
  };
102
+ if (nonSystem.length === 1 && nonSystem[0].role === "user") {
103
+ const extracted = extractUserContent(nonSystem[0].content, includeImages);
104
+ imageUrls.push(...extracted.imageUrls);
105
+ droppedImageCount += extracted.droppedImageCount;
106
+ return {
107
+ prompt: extracted.text,
108
+ system,
109
+ history: [],
110
+ imageUrls,
111
+ droppedImageCount,
112
+ ...extracted.input ? { input: extracted.input } : {}
113
+ };
114
+ }
102
115
  const last = nonSystem[nonSystem.length - 1];
103
- const prompt = last.role === "user" ? extractTextFromContent(last.content) : "";
116
+ let prompt = "";
117
+ let input;
118
+ if (last.role === "user") {
119
+ const extracted = extractUserContent(last.content, includeImages);
120
+ prompt = extracted.text;
121
+ imageUrls.push(...extracted.imageUrls);
122
+ droppedImageCount += extracted.droppedImageCount;
123
+ input = extracted.input;
124
+ }
104
125
  const historyEnd = last.role === "user" ? nonSystem.length - 1 : nonSystem.length;
105
126
  const history = [];
106
127
  for (let i = 0; i < historyEnd; i++) {
107
128
  const msg = nonSystem[i];
108
- if (msg.role === "user") history.push({
109
- type: "message",
110
- role: "user",
111
- content: [{
112
- type: "input_text",
113
- text: extractTextFromContent(msg.content)
114
- }]
115
- });
116
- else if (msg.role === "assistant") {
129
+ if (msg.role === "user") {
130
+ const extracted = extractUserContent(msg.content, includeImages);
131
+ imageUrls.push(...extracted.imageUrls);
132
+ droppedImageCount += extracted.droppedImageCount;
133
+ const content = codexContentItemsFromExtracted(extracted.parts);
134
+ history.push({
135
+ type: "message",
136
+ role: "user",
137
+ content: content.length > 0 ? content : [{
138
+ type: "input_text",
139
+ text: extracted.text
140
+ }]
141
+ });
142
+ } else if (msg.role === "assistant") {
117
143
  for (const part of msg.content) if ("text" in part && typeof part.text === "string") history.push({
118
144
  type: "message",
119
145
  role: "assistant",
@@ -143,7 +169,10 @@ function extractPromptAndHistory(messages) {
143
169
  return {
144
170
  prompt,
145
171
  system,
146
- history
172
+ history,
173
+ imageUrls,
174
+ droppedImageCount,
175
+ ...input ? { input } : {}
147
176
  };
148
177
  }
149
178
  function extractTextFromContent(content) {
@@ -152,6 +181,108 @@ function extractTextFromContent(content) {
152
181
  for (const part of content) if ("text" in part && typeof part.text === "string") parts.push(part.text);
153
182
  return parts.join("\n");
154
183
  }
184
+ function extractUserContent(content, includeImages) {
185
+ const extracted = extractMessageParts(content, { includeImages });
186
+ return {
187
+ text: extractTextFromContent(content),
188
+ parts: extracted.parts,
189
+ input: inputPartsFromExtracted(extracted.parts),
190
+ imageUrls: extracted.imageUrls,
191
+ droppedImageCount: extracted.droppedImageCount
192
+ };
193
+ }
194
+ function extractMessageParts(content, options) {
195
+ if (typeof content === "string") return {
196
+ parts: [{
197
+ kind: "text",
198
+ text: content
199
+ }],
200
+ imageUrls: [],
201
+ droppedImageCount: 0
202
+ };
203
+ const parts = [];
204
+ const imageUrls = [];
205
+ let droppedImageCount = 0;
206
+ for (const part of content) {
207
+ if ("text" in part && typeof part.text === "string") {
208
+ parts.push({
209
+ kind: "text",
210
+ text: part.text
211
+ });
212
+ continue;
213
+ }
214
+ const url = extractImageUrl(part);
215
+ if (!url) continue;
216
+ if (options.includeImages) {
217
+ parts.push({
218
+ kind: "image",
219
+ url
220
+ });
221
+ imageUrls.push(url);
222
+ } else droppedImageCount++;
223
+ }
224
+ return {
225
+ parts,
226
+ imageUrls,
227
+ droppedImageCount
228
+ };
229
+ }
230
+ function inputPartsFromExtracted(extractedParts) {
231
+ const parts = [];
232
+ for (const part of extractedParts) if (part.kind === "text") {
233
+ if (part.text.length > 0) parts.push({
234
+ type: "text",
235
+ text: part.text,
236
+ text_elements: []
237
+ });
238
+ } else parts.push({
239
+ type: "image",
240
+ url: part.url
241
+ });
242
+ return parts.some((part) => part.type === "image") ? parts : void 0;
243
+ }
244
+ function codexContentItemsFromExtracted(parts) {
245
+ const items = [];
246
+ for (const part of parts) if (part.kind === "text") items.push({
247
+ type: "input_text",
248
+ text: part.text
249
+ });
250
+ else items.push({
251
+ type: "input_image",
252
+ image_url: part.url
253
+ });
254
+ return items;
255
+ }
256
+ function extractImageUrl(part) {
257
+ if (!part || typeof part !== "object") return void 0;
258
+ const record = part;
259
+ const type = record.type;
260
+ if (type !== "file" && type !== "image") return void 0;
261
+ const mediaType = typeof record.mediaType === "string" ? record.mediaType : "image/png";
262
+ if (type === "file" && !mediaType.toLowerCase().startsWith("image/")) return void 0;
263
+ const data = record.data ?? record.image;
264
+ if (typeof data === "string") {
265
+ if (isSupportedImageUrl(data)) return data;
266
+ if (/^[a-z][a-z0-9+.-]*:/i.test(data)) return void 0;
267
+ return `data:${mediaType};base64,${data}`;
268
+ }
269
+ if (data instanceof URL) {
270
+ const url = data.toString();
271
+ return isSupportedImageUrl(url) ? url : void 0;
272
+ }
273
+ if (data instanceof Uint8Array) return `data:${mediaType};base64,${bytesToBase64(data)}`;
274
+ if (data instanceof ArrayBuffer) return `data:${mediaType};base64,${bytesToBase64(new Uint8Array(data))}`;
275
+ }
276
+ function isSupportedImageUrl(value) {
277
+ return /^(https?:|data:|blob:)/i.test(value);
278
+ }
279
+ function bytesToBase64(bytes) {
280
+ if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
281
+ const chunkSize = 32768;
282
+ let binary = "";
283
+ for (let i = 0; i < bytes.length; i += chunkSize) binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
284
+ return btoa(binary);
285
+ }
155
286
  function safeStringify(value) {
156
287
  try {
157
288
  const json = JSON.stringify(value);
@@ -471,6 +602,21 @@ function createQgridLogger(config = {}) {
471
602
  //#endregion
472
603
  //#region src/index.ts
473
604
  const threadCoordStore = /* @__PURE__ */ new Map();
605
+ const MAX_IMAGE_INPUT_DATA_URL_CHARS = 9e5;
606
+ const MAX_IMAGE_INPUT_DATA_URL_CHARS_LABEL = `${MAX_IMAGE_INPUT_DATA_URL_CHARS / 1e3}k`;
607
+ function assertImageInputFitsJsonTransport(imageUrls) {
608
+ let totalDataUrlChars = 0;
609
+ for (const url of imageUrls) {
610
+ if (!url.toLowerCase().startsWith("data:")) continue;
611
+ totalDataUrlChars += url.length;
612
+ if (url.length > MAX_IMAGE_INPUT_DATA_URL_CHARS) throw new Error(`qgrid: reference image input is too large for JSON transport (${url.length} chars). Compress or resize it before passing it to imageGeneration, for example as WebP/JPEG under ${MAX_IMAGE_INPUT_DATA_URL_CHARS_LABEL} total base64 data-url chars.`);
613
+ }
614
+ if (totalDataUrlChars > MAX_IMAGE_INPUT_DATA_URL_CHARS) throw new Error(`qgrid: reference image inputs are too large for JSON transport (${totalDataUrlChars} total chars). Compress or resize them before passing them to imageGeneration, for example as WebP/JPEG under ${MAX_IMAGE_INPUT_DATA_URL_CHARS_LABEL} total base64 data-url chars.`);
615
+ }
616
+ function warnDroppedImages(droppedImageCount) {
617
+ if (droppedImageCount <= 0) return;
618
+ console.warn(`[qgrid] ${droppedImageCount} image message part(s) were ignored because providerOptions.qgrid.imageGeneration is not enabled.`);
619
+ }
474
620
  function getThreadCoord(sessionKey) {
475
621
  const entry = threadCoordStore.get(sessionKey);
476
622
  if (!entry) return void 0;
@@ -522,14 +668,16 @@ function qgrid(modelId, config) {
522
668
  async doGenerate(options) {
523
669
  const tools = options.tools?.filter((t) => t.type === "function");
524
670
  const hasTools = tools && tools.length > 0;
525
- const { prompt, system, history } = extractPromptAndHistory(options.prompt);
526
671
  const qgridOptions = getQgridProviderOptions(options);
672
+ const imageGeneration = qgridOptions.imageGeneration;
673
+ const { prompt, system, history, input, imageUrls, droppedImageCount } = extractPromptAndHistory(options.prompt, { includeImages: imageGeneration === true });
674
+ if (!imageGeneration) warnDroppedImages(droppedImageCount);
527
675
  const effectiveEffort = qgridOptions.effort ?? effort;
528
676
  const verbosity = qgridOptions.verbosity;
529
677
  const reasoningSummary = qgridOptions.reasoningSummary;
530
678
  const serviceTier = qgridOptions.serviceTier;
531
- const imageGeneration = qgridOptions.imageGeneration;
532
679
  const imageGenerationOptions = qgridOptions.imageGenerationOptions;
680
+ if (imageGeneration) assertImageInputFitsJsonTransport(imageUrls);
533
681
  const sessionKey = qgridOptions.sessionKey;
534
682
  const reuseSessionKey = reusableSessionKey(modelId, sessionKey);
535
683
  const rawSchema = options.responseFormat?.type === "json" ? options.responseFormat.schema : void 0;
@@ -562,6 +710,7 @@ function qgrid(modelId, config) {
562
710
  headers: { "Content-Type": "application/json" },
563
711
  body: JSON.stringify({ args: {
564
712
  prompt,
713
+ ...imageGeneration && input ? { input } : {},
565
714
  model: modelId,
566
715
  system,
567
716
  effort: effectiveEffort,
@@ -639,13 +788,14 @@ function qgrid(modelId, config) {
639
788
  async doStream(options) {
640
789
  const tools = options.tools?.filter((t) => t.type === "function");
641
790
  const hasTools = tools && tools.length > 0;
642
- const { prompt, system, history } = extractPromptAndHistory(options.prompt);
643
791
  const qgridOptions = getQgridProviderOptions(options);
644
792
  const effectiveEffort = qgridOptions.effort ?? effort;
645
793
  const verbosity = qgridOptions.verbosity;
646
794
  const reasoningSummary = qgridOptions.reasoningSummary;
647
795
  const serviceTier = qgridOptions.serviceTier;
648
796
  if (qgridOptions.imageGeneration) throw new Error("qgrid: imageGeneration is not supported with streamText — use generateText instead.");
797
+ const { prompt, system, history, droppedImageCount } = extractPromptAndHistory(options.prompt, { includeImages: false });
798
+ warnDroppedImages(droppedImageCount);
649
799
  const sessionKey = qgridOptions.sessionKey;
650
800
  const reuseSessionKey = reusableSessionKey(modelId, sessionKey);
651
801
  const rawSchema = options.responseFormat?.type === "json" ? options.responseFormat.schema : void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartanova/qgrid-ai-sdk",
3
- "version": "2.3.9",
3
+ "version": "2.4.1",
4
4
  "description": "AI SDK LanguageModelV3 provider for qgrid",
5
5
  "repository": {
6
6
  "type": "git",