@cartanova/qgrid-ai-sdk 2.4.0 → 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 +19 -0
- package/README.md +19 -0
- package/dist/index.js +171 -21
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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")
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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;
|