@cartanova/qgrid-ai-sdk 2.10.5 → 2.10.6

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](./README.md) · **한국어**
4
4
 
5
- AI SDK v6 custom `LanguageModelV3` provider for [qgrid](https://github.com/cartanova-ai/Qgrid).
5
+ AI SDK v6 custom `LanguageModelV3` / `ImageModelV3` provider for [qgrid](https://github.com/cartanova-ai/Qgrid).
6
6
 
7
7
  **기존 AI SDK 코드 변경 없이, `model` 한 줄만 바꾸면 구독 토큰 풀링(토큰 N개 × concurrent permit) + request log 대시보드를 사용할 수 있습니다.**
8
8
 
@@ -211,8 +211,6 @@ const { text } = await generateText({
211
211
  | `reasoningSummary` | `"auto"` \| `"concise"` \| `"detailed"` \| `"none"` | OpenAI 전용 | 추론 요약 출력 방식 |
212
212
  | `serviceTier` | `string` | OpenAI 전용 | OpenAI/codex service tier |
213
213
  | `timeoutMs` | 양의 정수, 최대 `1_800_000` | Anthropic 전용 | 서버의 Claude Code 프로세스 제한시간(ms). SDK의 non-stream HTTP 제한은 이 값보다 60초 길게 설정. 기본값은 240초 |
214
- | `imageGeneration` | `boolean` | OpenAI 전용, non-stream | codex 내장 `image_generation` tool 활성화 ([아래](#image-generation) 참조) |
215
- | `imageGenerationOptions` | `{ quality?, size?, background? }` | OpenAI 전용 | 이미지 생성 옵션. `background: "transparent"`는 전용 경로 사용. 실제 크기·품질은 응답 metadata 확인 ([아래](#image-generation)). |
216
214
  | `fallbackModels` | `string[]` | deprecated | 호환용으로만 남긴 무시되는 필드. 사용하면 unsupported-option 경고를 반환하므로 호출부에서 제거하세요. Claude Code의 Fable refusal fallback과 무관 |
217
215
 
218
216
  ```typescript
@@ -270,52 +268,68 @@ const { text } = await generateText({
270
268
 
271
269
  ### Image Generation
272
270
 
273
- OpenAI/codex 경로 전용, `generateText` 전용입니다. 요청별로 이미지 생성을 활성화하고 결과를 AI SDK `files`로 받습니다. 투명 배경은 Codex 전용 Images 경로를, 그 외에는 내장 `image_generation` 도구를 사용합니다.
271
+ 이미지 생성과 참조 이미지 편집에는 표준 `generateImage`와 `qgrid.image()`를 사용합니다.
272
+ 구독 토큰으로 Codex Images 엔드포인트를 직접 호출하므로 Astra/Luna 같은 채팅 모델을
273
+ 선택하지 않습니다. 서버와 SDK를 이 인터페이스를 지원하는 버전으로 함께 업데이트하세요.
274
+
275
+ `openai/gpt-image-2`는 Codex 구독 경로의 요청 식별자이며 실제 이미지 모델 버전을 2.0으로
276
+ 고정한다는 뜻은 아닙니다. OpenAI는 Codex에 Images 2.5를 제공한다고 발표했지만, 구독
277
+ 엔드포인트에서 Flare/Sunburst 선택을 보장할 방법은 확인되지 않았습니다. 응답에 모델이
278
+ 없을 때 메타데이터의 모델명도 요청 식별자를 사용하므로 실제 버전의 증거로 해석하지 마세요.
274
279
 
275
280
  ```typescript
276
- const result = await generateText({
277
- model: qgrid("openai/gpt-5.6-terra"),
278
- prompt: "우주를 나는 고래 일러스트",
281
+ import { generateImage } from "ai";
282
+ import { qgrid, type QgridImageProviderOptions } from "@cartanova/qgrid-ai-sdk";
283
+
284
+ const { image, usage, providerMetadata } = await generateImage({
285
+ model: qgrid.image("openai/gpt-image-2", { projectName: "deti" }),
286
+ prompt: "세로형 포스터를 만들어줘",
279
287
  providerOptions: {
280
- qgrid: {
281
- imageGeneration: true,
282
- imageGenerationOptions: { quality: "medium", size: "1536x1024" },
283
- },
288
+ qgrid: { background: "opaque" } satisfies QgridImageProviderOptions,
284
289
  },
285
290
  });
286
291
 
287
- const image = result.files[0]; // mediaType: "image/png", base64
292
+ image.uint8Array; // 실제 반환된 PNG bytes
293
+ usage.inputTokens; // 이미지 모델 입력 토큰 (채팅 모델 usage가 아님)
294
+ providerMetadata.qgrid?.images[0]; // requestLogId, generation: 생성 조건과 세부 usage
288
295
  ```
289
296
 
290
- 레퍼런스 이미지는 일반 AI SDK multimodal message part로 전달할 수 있습니다:
297
+ 프로젝트 전체에는 `QGRID_PROJECT_NAME=deti` 설정을 권장합니다. 모델 설정에는 기존과 같이
298
+ `serverUrl`과 `projectName`을 사용할 수 있습니다. `providerOptions.qgrid`에는 `quality`,
299
+ `background`, `outputFormat: "png"`, `tokenName`, `logger`를 중첩 없이 전달합니다.
300
+ 표준 `size`는 provider options가 아닌 `generateImage` 최상위에 전달합니다.
301
+
302
+ 참조 이미지 편집도 표준 입력으로 전달합니다. `images`의 각 항목에는 `Buffer`,
303
+ `Uint8Array`, base64 이미지 데이터 또는 HTTPS URL 문자열을 넣습니다. 참조가 있으면
304
+ `/images/edits`, 없으면 `/images/generations`를 호출합니다. 큰 참조 사진은 압축하거나
305
+ 리사이즈해서 전달하세요. 과도하게 큰 인라인 이미지 입력은 전송 전에 거부합니다.
291
306
 
292
307
  ```typescript
293
- const result = await generateText({
294
- model: qgrid("openai/gpt-5.6-terra"),
295
- messages: [
296
- {
297
- role: "user",
298
- content: [
299
- { type: "text", text: "이 이미지를 스타일 레퍼런스로 사용해서 포스터를 만들어줘" },
300
- { type: "file", mediaType: "image/png", data: referenceImageBase64 },
301
- ],
302
- },
303
- ],
304
- providerOptions: { qgrid: { imageGeneration: true } },
308
+ const { image } = await generateImage({
309
+ model: qgrid.image("openai/gpt-image-2"),
310
+ prompt: { text: "이 캐릭터를 유지해서 포스터를 만들어줘", images: [referenceImageBytes] },
305
311
  });
306
312
  ```
307
313
 
308
- - `streamText`에서는 거부됩니다 (non-stream 전용).
309
- - 이미지 생성 요청은 provider 대화 상태를 보관하지 않고 전체 input을 직접 전송합니다.
310
- - 레퍼런스 이미지는 JSON data URL로 전송됩니다. 큰 사진은 압축하거나 리사이즈해서 전달하세요. 과도하게 큰 base64 입력은 SDK가 거부하며, 사진에는 WebP/JPEG를 권장합니다.
311
- - 일반 Responses 이미지 비용은 알 수 없어 `image_cost_usd: null`로 기록합니다. 요청한 크기·품질이나 텍스트 모델 사용량을 이미지 비용으로 간주하지 않습니다. 출력 metadata가 없는 기존 응답만 세 가지 표준 크기의 공개 가격표 추정을 유지하며, 임의 크기와 `auto`는 추정하지 않습니다.
312
- - `imageGenerationOptions.background`는 `"auto" | "opaque" | "transparent"`를 지원합니다. `"transparent"` 요청만 기존 ChatGPT 구독 토큰으로 Codex 전용 `images/generations` 또는 `images/edits` 경로를 사용합니다. 나머지는 기존 Responses 도구 경로를 유지합니다. 투명 경로는 텍스트 모델 없이 `gpt-image-2`를 직접 실행하며, tools·structured output은 지원하지 않습니다. 레퍼런스는 최대 5장입니다. 불투명·완전 빈 이미지·손상된 PNG는 오류로 처리하고, 별도 배경 제거로 대체하지 않습니다.
313
- - 두 경로 모두 `result.providerMetadata.qgrid.imageGeneration` 배열로 출력 정보를 제공합니다. `contentIndex`, `route`와 선택적인 `model`, `size`, `quality`, `background`, 이미지 `usage`가 포함됩니다. 경로는 `codex-responses` 또는 `codex-images`이며, 일반 경로에서 보고되지 않은 이미지 모델은 비워 둡니다. `result.content` 파일 파트에도 metadata가 있지만 `result.files[]`에는 없습니다. PNG 크기는 실제 바이트에서 읽고, JPEG/WebP는 백엔드가 보고한 출력 크기를 사용합니다. qgrid는 리사이즈하지 않습니다.
314
- - `imageGenerationOptions.size`는 `"auto"` 또는 `"1152x2560"` 같은 양의 정수 `가로x세로` 힌트를 받습니다. 정확한 출력 크기를 보장하지 않으므로 생략하거나 `auto`를 사용하고, 원하는 비율은 프롬프트에 적으세요. 2026-10-13 실호출에서 두 구독 경로 모두 크기 옵션을 무시했고, 9:20 요청은 `841x1870`, 4K 세로 요청은 `941x1672`, 4:1 요청은 3:1로 반환됐습니다. 약 157만 픽셀이라는 관측값은 모든 계정의 고정 상한이 아닙니다. 공개 Images API의 16배수·4K 규칙도 구독 경로의 보장 사양이 아닙니다. 확장된 크기 힌트와 일반 이미지 metadata를 사용하려면 서버와 SDK를 함께 업데이트하세요.
315
- - 투명 경로는 텍스트 모델을 실행하지 않아 일반 토큰 usage·비용이 0입니다. 이미지 usage는 별도로 보존하고, `image_cost_usd`는 보고된 토큰과 공개 API 단가로 추정합니다. 알려진 text/image input은 구분하고 불명확한 input은 보수적으로 계산하며, 캐시 할인은 반영하지 않습니다. 구독 청구액을 뜻하지 않습니다.
316
- - 이미지 도구 로그는 `requestedOptions`와 `observedGeneration`을 구분합니다. 관측 정보가 없고 가격표에 있는 크기의 기존 응답만 `pricingAssumption`을 유지합니다. 그 기본값 `1536x1024`/`medium`은 실제 출력 설정을 뜻하지 않습니다.
317
-
318
- 투명 생성은 위 `imageGenerationOptions`에 `background: "transparent"`를 추가하고, 프롬프트에도 피사체 밖의 투명 배경을 명시하세요. 크로마키·배경색 지시는 제거해야 합니다. 이 기능이 포함된 서버와 SDK가 모두 필요하며, 저장 시 PNG 알파를 유지하세요. 정확한 최종 크기는 소비 앱의 명시적인 crop/resize 정책으로 처리합니다.
314
+ - PNG/JPEG/WebP 참조 이미지는 요청당 최대 5개이고, 출력은 PNG입니다. 마스크 편집과 JPEG/WebP 출력은 지원하지 않습니다.
315
+ - `n`은 AI SDK가 한 장씩 여러 호출로 나눕니다. 요청당 한 장 제한을 `maxImagesPerCall`로 우회할 수는 없습니다.
316
+ - `size`와 `quality`는 힌트이며 구독 백엔드가 자동 결정할 수 있습니다. 실제 조건은 메타데이터에서 확인하세요. `aspectRatio`와 `seed`는 적용되지 않으며 AI SDK 경고를 반환합니다. 원하는 비율은 프롬프트로 전달하세요.
317
+ - `usage`는 이미지 응답의 토큰 사용량입니다. 세부 입력 분류는 `providerMetadata.qgrid.images[]`에 보존합니다. 서버는 이 usage로 API 가격 기준 추정 비용을 기록하고, usage가 없으면 이미지당 가격 추정을 사용합니다. 별도 채팅 비용은 없습니다.
318
+ - `logger: false`는 요청 로그만 비활성화합니다. 취소는 표준 `abortSignal`을 사용합니다.
319
+
320
+ 투명 출력은 `background: "transparent"`를 중첩 없이 전달하고 프롬프트에도 투명 배경을
321
+ 명시하세요. 크로마키·배경색 지시는 제거하고, 저장 시 PNG 알파를 유지하세요. 불투명·완전
322
+ 빈 이미지·손상된 PNG는 오류로 처리합니다. qgrid는 배경 제거 또는 리사이즈를 하지 않으며,
323
+ 정확한 최종 크기는 소비 앱의 명시적인 crop/resize 정책으로 처리합니다.
324
+
325
+ `generateText`와 `streamText`는 이미지 생성 옵션이 없는 대화 모델 인터페이스입니다.
326
+ 일반적인 이미지 인식도 지원하지 않아 image part는 경고와 함께 무시됩니다. 이미지 생성·편집이
327
+ 목적이면 `generateImage`의 참조 입력을 사용하세요. 대시보드 `qgrid_chat`은 내부 이미지
328
+ 도구를 기본으로 등록합니다. 채팅 모델이 생성 여부를 판단하고 완결된 프롬프트를 작성하며,
329
+ 편집 시 최근 대화 이미지 중 최대 5개를 선택합니다. 서버는 일반·투명 이미지 모두 구독
330
+ Images를 직접 호출하고 요청 로그에는 채팅과 이미지 usage의 추정 비용을 합산합니다.
331
+ 텍스트만 응답할 수도 있습니다. 공개 SDK 도구 팩토리는 제공하지 않으며, 자동 채팅 이미지도
332
+ `generateImage`와 같이 PNG만 지원합니다.
319
333
 
320
334
  ## Telemetry Logger
321
335
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **English** · [한국어](./README.ko.md)
4
4
 
5
- AI SDK v6 custom `LanguageModelV3` provider for [qgrid](https://github.com/cartanova-ai/Qgrid).
5
+ AI SDK v6 custom `LanguageModelV3` / `ImageModelV3` provider for [qgrid](https://github.com/cartanova-ai/Qgrid).
6
6
 
7
7
  **Without changing your existing AI SDK code, swap a single `model` line to get subscription-token pooling (N tokens × concurrent permits) + the request log dashboard.**
8
8
 
@@ -220,8 +220,6 @@ const { text } = await generateText({
220
220
  | `reasoningSummary` | `"auto"` \| `"concise"` \| `"detailed"` \| `"none"` | OpenAI only | Reasoning summary output mode |
221
221
  | `serviceTier` | `string` | OpenAI only | OpenAI/codex service tier |
222
222
  | `timeoutMs` | positive integer, max `1_800_000` | Anthropic only | Server-side Claude Code process timeout in milliseconds. The SDK's non-stream HTTP budget is 60 seconds longer. Defaults to 240 seconds |
223
- | `imageGeneration` | `boolean` | OpenAI only, non-stream | Enables codex's built-in `image_generation` tool (see [below](#image-generation)) |
224
- | `imageGenerationOptions` | `{ quality?, size?, background?, outputFormat? }` | OpenAI only | Image controls; `outputFormat` selects PNG/JPEG/WebP on Responses. `background: "transparent"` selects standalone Images (PNG only). Read actual size/quality from response metadata ([below](#image-generation)). |
225
223
  | `fallbackModels` | `string[]` | deprecated | Ignored compatibility field; emits an unsupported-option warning. Remove it from callers. Unrelated to Claude Code's Fable refusal fallback |
226
224
 
227
225
  ```typescript
@@ -279,78 +277,69 @@ const { text } = await generateText({
279
277
 
280
278
  ### Image Generation
281
279
 
282
- OpenAI/codex route only, `generateText` only. Enables image generation for that single request and returns the image through AI SDK `files`. Transparent backgrounds use standalone Codex Images; other requests use the hosted `image_generation` tool.
280
+ Use standard AI SDK `generateImage` with `qgrid.image()` for image generation and reference
281
+ editing. This calls the Codex subscription Images endpoint directly, without an Astra/Luna
282
+ chat driver. Update the server and SDK together to versions supporting this interface.
283
+
284
+ `openai/gpt-image-2` is the Codex wire request identifier, not a guarantee of Images 2.0
285
+ serving weights. OpenAI announced Images 2.5 in Codex, but Flare/Sunburst selection is not
286
+ established on this subscription endpoint. When the response omits its model, metadata falls
287
+ back to the requested identifier; that fallback is not evidence of the serving version.
283
288
 
284
289
  ```typescript
285
- const result = await generateText({
286
- model: qgrid("openai/gpt-5.6-terra"),
287
- prompt: "An illustration of a whale flying through space",
290
+ import { generateImage } from "ai";
291
+ import { qgrid, type QgridImageProviderOptions } from "@cartanova/qgrid-ai-sdk";
292
+
293
+ const { image, usage, providerMetadata } = await generateImage({
294
+ model: qgrid.image("openai/gpt-image-2", { projectName: "deti" }),
295
+ prompt: "Create a portrait poster",
288
296
  providerOptions: {
289
- qgrid: {
290
- imageGeneration: true,
291
- imageGenerationOptions: { quality: "medium", size: "1536x1024", outputFormat: "webp" },
292
- },
297
+ qgrid: { background: "opaque" } satisfies QgridImageProviderOptions,
293
298
  },
294
299
  });
295
300
 
296
- const image = result.files[0]; // mediaType: "image/webp", base64
301
+ image.uint8Array; // Returned PNG bytes
302
+ usage.inputTokens; // Image-model input tokens, not chat-model usage
303
+ providerMetadata.qgrid?.images[0]; // requestLogId and generation settings/detailed usage
297
304
  ```
298
305
 
299
- Since 2.9.6, `imageGenerationOptions.outputFormat` accepts `"png" | "jpeg" | "webp"`; omission keeps PNG. qgrid preserves the returned image format in `mediaType` rather than converting the image. Save files with the matching extension. Transparent background requests only support PNG and reject JPEG/WebP before generation. `result.files[].mediaType` retains the AI SDK standard `string` type; no qgrid-specific wrapper or type assertion is needed.
306
+ Prefer `QGRID_PROJECT_NAME=deti` for the project-wide label. The image factory accepts
307
+ `serverUrl` and `projectName`. `providerOptions.qgrid` takes flat image options: `quality`,
308
+ `background`, `outputFormat: "png"`, `tokenName`, and `logger`. Standard `size` belongs at
309
+ the `generateImage` top level, not inside the provider options.
300
310
 
301
- Reference images can be passed through normal AI SDK multimodal message parts:
311
+ Reference editing uses the standard image prompt. Pass `Buffer`, `Uint8Array`, base64 image
312
+ data, or an HTTPS URL string in `images`. References select `/images/edits`; omitting them selects
313
+ `/images/generations`. Compress or resize large reference photos before sending them; oversized
314
+ inline image inputs are rejected before transport.
302
315
 
303
316
  ```typescript
304
- const result = await generateText({
305
- model: qgrid("openai/gpt-5.6-terra"),
306
- messages: [
307
- {
308
- role: "user",
309
- content: [
310
- { type: "text", text: "Use this image as a style reference and create a poster" },
311
- { type: "file", mediaType: "image/png", data: referenceImageBase64 },
312
- ],
313
- },
314
- ],
315
- providerOptions: { qgrid: { imageGeneration: true } },
316
- });
317
- ```
318
-
319
- - Rejected in `streamText` (non-stream only).
320
- - Image-generation requests do not retain provider conversation state. Their full input is sent directly.
321
- - 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.
322
- - Hosted Responses image cost is unknown (`image_cost_usd: null`): requested size/quality and text-driver usage cannot determine image cost. Standalone image usage is priced separately as described below. Legacy responses without generation metadata retain the three-size public price estimate; custom sizes and `auto` never receive an invented estimate.
323
- - `imageGenerationOptions.background` accepts `"auto" | "opaque" | "transparent"`. Only `"transparent"` uses Codex's standalone `images/generations` or `images/edits` endpoint with the existing ChatGPT subscription token. Other image requests keep the hosted Responses tool. The transparent route runs `gpt-image-2` directly, without a text driver; it rejects tools and structured output. It accepts up to five reference images and preserves PNG bytes. Fully opaque, entirely empty, or invalid PNG results fail explicitly; there is no background-removal fallback.
324
- - Both routes expose output metadata in `result.providerMetadata.qgrid.imageGeneration` (an array with `contentIndex`, `route`, and optional `model`, `size`, `quality`, `background`, and image `usage`). Routes are `codex-responses` and `codex-images`. An unreported hosted image model stays absent; the text driver is not the image model. Per-file metadata also appears on `result.content` file parts, not on `result.files[]`. PNG dimensions come from the returned bytes; JPEG/WebP dimensions use the backend's reported output size when available. qgrid does not resize output.
325
- - `imageGenerationOptions.size` accepts `"auto"` or positive integer `WIDTHxHEIGHT` hints, such as `"1152x2560"`. These are not exact-output guarantees. Omit size or use `auto`, and put the desired aspect ratio in the prompt (for example, "9:20 portrait"). On 2026-10-13, both subscription routes ignored explicit size, returned `841x1870` for a 9:20 prompt, `941x1672` for a 4K portrait request, and 3:1 for a 4:1 prompt. The observed approximately 1.57MP output is not a guaranteed account-wide limit. Public Images API rules (16-pixel multiples and up to 4K) do not establish subscription output behavior. Update server and SDK together to use expanded size hints and hosted metadata.
326
- - On the transparent route, regular token usage/cost is zero because no text driver ran. Image usage is preserved separately and `image_cost_usd` is an API-price estimate from reported usage, with known text/image input splits and conservative pricing for unknown input. It omits cache discounts and is not subscription billing.
327
- - Image logs distinguish `requestedOptions` from `observedGeneration`. Only legacy responses without observations and with a known size price retain `pricingAssumption`; its `1536x1024`/`medium` defaults never establish actual output settings.
328
-
329
- Transparent generation example (requires a server and SDK containing this feature):
330
-
331
- ```typescript
332
- import { generateText } from "ai";
333
- import { qgrid, type QgridProviderOptions } from "@cartanova/qgrid-ai-sdk";
334
-
335
- const result = await generateText({
336
- model: qgrid("openai/gpt-6.1-sol", { projectName: "deti" }),
337
- prompt: "An isolated character. Everything outside the character must be transparent, with no backdrop or checkerboard.",
338
- providerOptions: {
339
- qgrid: {
340
- imageGeneration: true,
341
- imageGenerationOptions: {
342
- background: "transparent",
343
- quality: "high",
344
- size: "1024x1536",
345
- },
346
- } satisfies QgridProviderOptions,
347
- },
317
+ const { image } = await generateImage({
318
+ model: qgrid.image("openai/gpt-image-2"),
319
+ prompt: { text: "Keep this character and create a poster", images: [referenceImageBytes] },
348
320
  });
349
- const png = result.files[0];
350
- const observed = result.providerMetadata?.qgrid?.imageGeneration;
351
321
  ```
352
322
 
353
- Remove green-screen or other background instructions when requesting transparency. Preserve the returned PNG alpha when saving; exact final dimensions require an explicit downstream crop/resize policy.
323
+ - Up to five PNG/JPEG/WebP references per request; output is PNG. Masks and JPEG/WebP output are unsupported.
324
+ - AI SDK splits `n` into one-image calls. Do not override `maxImagesPerCall` to bypass that backend limit.
325
+ - `size` and `quality` are hints: the subscription backend can choose different values. Read observed settings from metadata. `aspectRatio` and `seed` are unsupported and return AI SDK warnings; describe the desired ratio in the prompt.
326
+ - `usage` contains the image response's token usage. Detailed input categories are preserved under `providerMetadata.qgrid.images[]`. The server records an API-price estimate from this usage, or falls back to per-image pricing when usage is absent. There is no separate chat-driver cost.
327
+ - `logger: false` disables request logging only. Cancellation uses standard `abortSignal`.
328
+
329
+ For transparent output, set the flat option `background: "transparent"` and describe transparency
330
+ in the prompt. Remove green-screen or other background instructions and preserve the returned PNG
331
+ alpha when saving. Fully opaque, entirely empty, or invalid PNG results fail explicitly; qgrid does
332
+ not remove backgrounds or resize output. Apply any final crop/resize policy in the consuming app.
333
+
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
336
+ `generateImage` references when the intended result is a generated or edited image. Dashboard
337
+ `qgrid_chat` registers an internal image tool by default: its chat model decides whether to
338
+ generate an image, writes a self-contained prompt, and selects up to five recent conversation
339
+ images for editing. The server calls subscription Images directly for ordinary and transparent
340
+ output, then combines separate chat and image usage estimates in the request log. Chat may
341
+ still return text only. No public SDK image-tool factory is required; automatic chat images
342
+ are PNG-only, like `generateImage`.
354
343
 
355
344
  ## Telemetry Logger
356
345
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { LanguageModelV3 } from "@ai-sdk/provider";
1
+ import { ImageModelV3, LanguageModelV3 } from "@ai-sdk/provider";
2
2
  import { TelemetrySettings } from "ai";
3
3
  //#region src/index.types.d.ts
4
4
  /**
@@ -21,6 +21,21 @@ type QgridCommonProviderConfig = {
21
21
  /** request_logs.project_name. default: process.env.QGRID_PROJECT_NAME */
22
22
  projectName?: string;
23
23
  };
24
+ /** Codex Images request ID, not a guaranteed serving version; does not select a chat model. */
25
+ type QgridImageModel = "openai/gpt-image-2";
26
+ type QgridImageProviderConfig = QgridCommonProviderConfig;
27
+ /** `providerOptions.qgrid` for `generateImage({ model: qgrid.image(...) })`. */
28
+ type QgridImageProviderOptions = {
29
+ /** Requested hint only; the subscription backend may choose another quality. */
30
+ quality?: "low" | "medium" | "high";
31
+ background?: "auto" | "opaque" | "transparent";
32
+ /** The standalone subscription Images endpoint currently supports PNG only. */
33
+ outputFormat?: "png";
34
+ /** Strict active OpenAI token selection, including the `openai/` prefix. */
35
+ tokenName?: string;
36
+ /** Defaults to true. Set false to omit qgrid request logs. */
37
+ logger?: boolean;
38
+ };
24
39
  type QgridOpenAIProviderConfig = QgridCommonProviderConfig & {
25
40
  defaultEffort?: QgridOpenAIEffort;
26
41
  };
@@ -68,26 +83,6 @@ type QgridOpenAIProviderOptions = QgridCommonProviderOptions & {
68
83
  reasoningSummary?: "auto" | "concise" | "detailed" | "none";
69
84
  /** Codex service tier. */
70
85
  serviceTier?: string;
71
- /**
72
- * OpenAI Responses image_generation tool 을 켠다. non-stream 전용.
73
- * generateText 결과의 files 로 이미지를 받는다. streaming(streamText)에서는 거부된다.
74
- */
75
- imageGeneration?: boolean;
76
- /**
77
- * Image generation controls. The Responses backend selects the image model;
78
- * Size/quality are hints, not output guarantees. Hosted image cost is unknown;
79
- * standalone usage is estimated using public image-token prices.
80
- */
81
- imageGenerationOptions?: {
82
- /** Responses output format. Defaults to PNG; transparent output only supports PNG. */
83
- outputFormat?: "png" | "jpeg" | "webp";
84
- quality?: "low" | "medium" | "high";
85
- /** Requested hint only; Codex may ignore it. Put the desired aspect ratio in the prompt.
86
- * Omit or use auto for backend sizing; read actual output size from generation metadata. */
87
- size?: "auto" | `${number}x${number}`;
88
- /** Transparent uses Codex standalone Images and validates PNG alpha. See README for limits. */
89
- background?: "auto" | "opaque" | "transparent";
90
- };
91
86
  };
92
87
  /** `providerOptions.qgrid` — Anthropic(Claude Code) 모델용. */
93
88
  type QgridAnthropicProviderOptions = QgridCommonProviderOptions & {
@@ -159,11 +154,17 @@ type QgridLoggerConfig = {
159
154
  onLogError?: (error: Error) => void;
160
155
  };
161
156
  //#endregion
157
+ //#region src/image-model.d.ts
158
+ declare function createQgridImage(modelId: QgridImageModel, config?: QgridImageProviderConfig): ImageModelV3;
159
+ //#endregion
162
160
  //#region src/logger.d.ts
163
161
  declare function createQgridLogger(config?: QgridLoggerConfig): TelemetrySettings;
164
162
  //#endregion
165
163
  //#region src/index.d.ts
166
164
  declare function qgrid(modelId: QgridOpenAIModel, config?: QgridOpenAIProviderConfig): LanguageModelV3;
167
165
  declare function qgrid(modelId: QgridAnthropicModel, config?: QgridAnthropicProviderConfig): LanguageModelV3;
166
+ declare namespace qgrid {
167
+ var image: typeof createQgridImage;
168
+ }
168
169
  //#endregion
169
- export { type QgridAnthropicEffort, type QgridAnthropicModel, type QgridAnthropicProviderConfig, type QgridAnthropicProviderOptions, type QgridImageGenerationMetadata, type QgridLoggerConfig, type QgridOpenAIEffort, type QgridOpenAIModel, type QgridOpenAIProviderConfig, type QgridOpenAIProviderOptions, type QgridProviderConfig, type QgridProviderOptions, type QgridSupportedModel, createQgridLogger, qgrid as default, qgrid };
170
+ export { type QgridAnthropicEffort, type QgridAnthropicModel, type QgridAnthropicProviderConfig, type QgridAnthropicProviderOptions, type QgridImageGenerationMetadata, type QgridImageModel, type QgridImageProviderConfig, type QgridImageProviderOptions, type QgridLoggerConfig, type QgridOpenAIEffort, type QgridOpenAIModel, type QgridOpenAIProviderConfig, type QgridOpenAIProviderOptions, type QgridProviderConfig, type QgridProviderOptions, type QgridSupportedModel, createQgridLogger, qgrid as default, qgrid };
package/dist/index.js CHANGED
@@ -434,53 +434,43 @@ async function* parseSSE(body, signal) {
434
434
  }
435
435
  }
436
436
  }
437
- function extractPromptAndHistory(messages, options = {}) {
438
- const includeImages = options.includeImages ?? true;
439
- const imageUrls = [];
437
+ function extractPromptAndHistory(messages) {
440
438
  let droppedImageCount = 0;
441
439
  let system;
442
440
  const nonSystem = [];
443
- for (const msg of messages) if (msg.role === "system") system = extractTextFromContent(msg.content);
441
+ for (const msg of messages) if (msg.role === "system") system = extractPromptTextParts(msg.content).text;
444
442
  else nonSystem.push(msg);
445
443
  if (nonSystem.length === 0) return {
446
444
  prompt: "",
447
445
  system,
448
446
  history: [],
449
- imageUrls,
450
447
  droppedImageCount
451
448
  };
452
449
  if (nonSystem.length === 1 && nonSystem[0].role === "user") {
453
- const extracted = extractUserContent(nonSystem[0].content, includeImages);
454
- imageUrls.push(...extracted.imageUrls);
450
+ const extracted = extractPromptTextParts(nonSystem[0].content);
455
451
  droppedImageCount += extracted.droppedImageCount;
456
452
  return {
457
453
  prompt: extracted.text,
458
454
  system,
459
455
  history: [],
460
- imageUrls,
461
- droppedImageCount,
462
- ...extracted.input ? { input: extracted.input } : {}
456
+ droppedImageCount
463
457
  };
464
458
  }
465
459
  const last = nonSystem[nonSystem.length - 1];
466
460
  let prompt = "";
467
- let input;
468
461
  if (last.role === "user") {
469
- const extracted = extractUserContent(last.content, includeImages);
462
+ const extracted = extractPromptTextParts(last.content);
470
463
  prompt = extracted.text;
471
- imageUrls.push(...extracted.imageUrls);
472
464
  droppedImageCount += extracted.droppedImageCount;
473
- input = extracted.input;
474
465
  }
475
466
  const historyEnd = last.role === "user" ? nonSystem.length - 1 : nonSystem.length;
476
467
  const history = [];
477
468
  for (let i = 0; i < historyEnd; i++) {
478
469
  const msg = nonSystem[i];
479
470
  if (msg.role === "user") {
480
- const extracted = extractUserContent(msg.content, includeImages);
481
- imageUrls.push(...extracted.imageUrls);
471
+ const extracted = extractPromptTextParts(msg.content);
482
472
  droppedImageCount += extracted.droppedImageCount;
483
- const content = responsesContentItemsFromExtracted(extracted.parts);
473
+ const content = extracted.parts;
484
474
  history.push({
485
475
  type: "message",
486
476
  role: "user",
@@ -489,36 +479,23 @@ function extractPromptAndHistory(messages, options = {}) {
489
479
  text: extracted.text
490
480
  }]
491
481
  });
492
- } else if (msg.role === "assistant") for (const part of msg.content) if ("text" in part && typeof part.text === "string") history.push({
493
- type: "message",
494
- role: "assistant",
495
- content: [{
496
- type: "output_text",
497
- text: part.text
498
- }]
499
- });
500
- else if ("toolName" in part && part.type === "tool-call") history.push({
501
- type: "function_call",
502
- name: part.toolName,
503
- arguments: typeof part.input === "string" ? part.input : JSON.stringify(part.input),
504
- call_id: part.toolCallId
505
- });
506
- else {
507
- const url = extractImageUrl(part);
508
- if (!url) continue;
509
- if (includeImages) {
510
- imageUrls.push(url);
511
- history.push({
512
- type: "message",
513
- role: "assistant",
514
- content: [{
515
- type: "input_image",
516
- image_url: url
517
- }]
518
- });
519
- } else droppedImageCount++;
520
- }
521
- else if (msg.role === "tool") {
482
+ } else if (msg.role === "assistant") {
483
+ for (const part of msg.content) if ("text" in part && typeof part.text === "string") history.push({
484
+ type: "message",
485
+ role: "assistant",
486
+ content: [{
487
+ type: "output_text",
488
+ text: part.text
489
+ }]
490
+ });
491
+ else if ("toolName" in part && part.type === "tool-call") history.push({
492
+ type: "function_call",
493
+ name: part.toolName,
494
+ arguments: typeof part.input === "string" ? part.input : JSON.stringify(part.input),
495
+ call_id: part.toolCallId
496
+ });
497
+ else if (isImagePart(part)) droppedImageCount++;
498
+ } else if (msg.role === "tool") {
522
499
  for (const part of msg.content) if ("toolName" in part && part.type === "tool-result") {
523
500
  const output = part.output;
524
501
  const text = "value" in output ? typeof output.value === "string" ? output.value : JSON.stringify(output.value) : JSON.stringify(output);
@@ -534,111 +511,35 @@ function extractPromptAndHistory(messages, options = {}) {
534
511
  prompt,
535
512
  system,
536
513
  history,
537
- imageUrls,
538
- droppedImageCount,
539
- ...input ? { input } : {}
540
- };
541
- }
542
- function extractTextFromContent(content) {
543
- if (typeof content === "string") return content;
544
- const parts = [];
545
- for (const part of content) if ("text" in part && typeof part.text === "string") parts.push(part.text);
546
- return parts.join("\n");
547
- }
548
- function extractUserContent(content, includeImages) {
549
- const extracted = extractMessageParts(content, { includeImages });
550
- return {
551
- text: extractTextFromContent(content),
552
- parts: extracted.parts,
553
- input: inputPartsFromExtracted(extracted.parts),
554
- imageUrls: extracted.imageUrls,
555
- droppedImageCount: extracted.droppedImageCount
514
+ droppedImageCount
556
515
  };
557
516
  }
558
- function extractMessageParts(content, options) {
517
+ function extractPromptTextParts(content) {
559
518
  if (typeof content === "string") return {
519
+ text: content,
560
520
  parts: [{
561
- kind: "text",
521
+ type: "input_text",
562
522
  text: content
563
523
  }],
564
- imageUrls: [],
565
524
  droppedImageCount: 0
566
525
  };
567
526
  const parts = [];
568
- const imageUrls = [];
569
527
  let droppedImageCount = 0;
570
- for (const part of content) {
571
- if ("text" in part && typeof part.text === "string") {
572
- parts.push({
573
- kind: "text",
574
- text: part.text
575
- });
576
- continue;
577
- }
578
- const url = extractImageUrl(part);
579
- if (!url) continue;
580
- if (options.includeImages) {
581
- parts.push({
582
- kind: "image",
583
- url
584
- });
585
- imageUrls.push(url);
586
- } else droppedImageCount++;
587
- }
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++;
588
533
  return {
534
+ text: parts.map((part) => part.text).join("\n"),
589
535
  parts,
590
- imageUrls,
591
536
  droppedImageCount
592
537
  };
593
538
  }
594
- function inputPartsFromExtracted(extractedParts) {
595
- const parts = [];
596
- for (const part of extractedParts) if (part.kind === "text") {
597
- if (part.text.length > 0) parts.push({
598
- type: "text",
599
- text: part.text,
600
- text_elements: []
601
- });
602
- } else parts.push({
603
- type: "image",
604
- url: part.url
605
- });
606
- return parts.some((part) => part.type === "image") ? parts : void 0;
607
- }
608
- function responsesContentItemsFromExtracted(parts) {
609
- const items = [];
610
- for (const part of parts) if (part.kind === "text") items.push({
611
- type: "input_text",
612
- text: part.text
613
- });
614
- else items.push({
615
- type: "input_image",
616
- image_url: part.url
617
- });
618
- return items;
619
- }
620
- function extractImageUrl(part) {
621
- if (!part || typeof part !== "object") return void 0;
539
+ function isImagePart(part) {
540
+ if (!part || typeof part !== "object") return false;
622
541
  const record = part;
623
- const type = record.type;
624
- if (type !== "file" && type !== "image") return void 0;
625
- const mediaType = typeof record.mediaType === "string" ? record.mediaType : "image/png";
626
- if (type === "file" && !mediaType.toLowerCase().startsWith("image/")) return void 0;
627
- const data = record.data ?? record.image;
628
- if (typeof data === "string") {
629
- if (isSupportedImageUrl(data)) return data;
630
- if (/^[a-z][a-z0-9+.-]*:/i.test(data)) return void 0;
631
- return `data:${mediaType};base64,${data}`;
632
- }
633
- if (data instanceof URL) {
634
- const url = data.toString();
635
- return isSupportedImageUrl(url) ? url : void 0;
636
- }
637
- if (data instanceof Uint8Array) return `data:${mediaType};base64,${bytesToBase64(data)}`;
638
- if (data instanceof ArrayBuffer) return `data:${mediaType};base64,${bytesToBase64(new Uint8Array(data))}`;
639
- }
640
- function isSupportedImageUrl(value) {
641
- return /^(https?:|data:|blob:)/i.test(value);
542
+ return record.type === "image" || record.type === "file" && typeof record.mediaType === "string" && record.mediaType.toLowerCase().startsWith("image/");
642
543
  }
643
544
  function bytesToBase64(bytes) {
644
545
  if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
@@ -647,6 +548,17 @@ function bytesToBase64(bytes) {
647
548
  for (let i = 0; i < bytes.length; i += chunkSize) binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
648
549
  return btoa(binary);
649
550
  }
551
+ const MAX_IMAGE_INPUT_DATA_URL_CHARS = 9e6;
552
+ const MAX_IMAGE_INPUT_DATA_URL_CHARS_LABEL = "9M";
553
+ function assertImageInputFitsJsonTransport(imageUrls) {
554
+ let totalDataUrlChars = 0;
555
+ for (const url of imageUrls) {
556
+ if (!url.toLowerCase().startsWith("data:")) continue;
557
+ 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.`);
559
+ }
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.`);
561
+ }
650
562
  function safeStringify(value) {
651
563
  try {
652
564
  const json = JSON.stringify(value);
@@ -716,6 +628,126 @@ function serializeHistory(messages) {
716
628
  return safeStringify(history);
717
629
  }
718
630
  //#endregion
631
+ //#region src/image-model.ts
632
+ function imageInputUrl(file) {
633
+ if (file.type === "url") {
634
+ if (!/^https:\/\//i.test(file.url)) throw new Error("qgrid.image: reference image URLs must use HTTPS");
635
+ return file.url;
636
+ }
637
+ if (![
638
+ "image/png",
639
+ "image/jpeg",
640
+ "image/webp"
641
+ ].includes(file.mediaType)) throw new Error("qgrid.image: reference images must be PNG, JPEG, or WebP");
642
+ const data = typeof file.data === "string" ? file.data : bytesToBase64(file.data);
643
+ return `data:${file.mediaType};base64,${data}`;
644
+ }
645
+ function createQgridImage(modelId, config) {
646
+ if (modelId !== "openai/gpt-image-2") throw new Error(`qgrid.image: unsupported image model ${modelId}`);
647
+ const serverUrl = config?.serverUrl ?? process.env.QGRID_URL ?? "http://localhost:44900";
648
+ const projectName = config?.projectName ?? process.env.QGRID_PROJECT_NAME;
649
+ return {
650
+ specificationVersion: "v3",
651
+ provider: QGRID_PROVIDER_NAME,
652
+ modelId,
653
+ maxImagesPerCall: 1,
654
+ async doGenerate(options) {
655
+ if (options.mask !== void 0) throw new Error("qgrid.image: masks are not supported by the subscription Images endpoint");
656
+ 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");
657
+ if (!options.prompt?.trim()) throw new Error("qgrid.image: a non-empty text prompt is required");
658
+ if ((options.files?.length ?? 0) > 5) throw new Error("qgrid.image: at most five reference images are supported");
659
+ const qgridOptions = options.providerOptions.qgrid ?? {};
660
+ if (qgridOptions.tokenName !== void 0 && (typeof qgridOptions.tokenName !== "string" || !/^openai\/.+/.test(qgridOptions.tokenName.trim()))) throw new Error("qgrid.image: tokenName must be a non-empty OpenAI token name with the openai/ prefix");
661
+ if (qgridOptions.logger !== void 0 && typeof qgridOptions.logger !== "boolean") throw new Error("qgrid.image: logger must be a boolean");
662
+ if (qgridOptions.outputFormat !== void 0 && qgridOptions.outputFormat !== "png") throw new Error("qgrid.image: the subscription Images endpoint supports PNG output only");
663
+ if (qgridOptions.quality !== void 0 && ![
664
+ "low",
665
+ "medium",
666
+ "high"
667
+ ].includes(qgridOptions.quality)) throw new Error("qgrid.image: quality must be low, medium, or high");
668
+ if (qgridOptions.background !== void 0 && ![
669
+ "auto",
670
+ "opaque",
671
+ "transparent"
672
+ ].includes(qgridOptions.background)) throw new Error("qgrid.image: background must be auto, opaque, or transparent");
673
+ 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");
674
+ const imageUrls = (options.files ?? []).map(imageInputUrl);
675
+ assertImageInputFitsJsonTransport(imageUrls);
676
+ const input = [{
677
+ type: "text",
678
+ text: options.prompt,
679
+ text_elements: []
680
+ }, ...imageUrls.map((url) => ({
681
+ type: "image",
682
+ url
683
+ }))];
684
+ const warnings = [];
685
+ for (const feature of ["seed", "aspectRatio"]) if (options[feature] !== void 0) warnings.push({
686
+ type: "unsupported",
687
+ feature,
688
+ details: "The subscription Images endpoint does not support this setting. Describe the desired result in the prompt."
689
+ });
690
+ for (const feature of [...options.size !== void 0 ? ["size"] : [], ...qgridOptions.quality !== void 0 ? ["providerOptions.qgrid.quality"] : []]) warnings.push({
691
+ type: "compatibility",
692
+ feature,
693
+ details: "This is a hint only. The subscription backend may choose a different size or quality; inspect the returned generation metadata."
694
+ });
695
+ const timestamp = /* @__PURE__ */ new Date();
696
+ const headers = new Headers();
697
+ for (const [key, value] of Object.entries(options.headers ?? {})) if (value !== void 0) headers.set(key, value);
698
+ headers.set("Content-Type", "application/json");
699
+ const response = await qgridFetch(`${serverUrl}/api/qgrid/query`, {
700
+ method: "POST",
701
+ headers,
702
+ signal: options.abortSignal,
703
+ body: JSON.stringify({ args: {
704
+ prompt: options.prompt,
705
+ model: modelId,
706
+ imageGeneration: true,
707
+ imageGenerationOptions: {
708
+ ...options.size !== void 0 ? { size: options.size } : {},
709
+ ...qgridOptions.quality !== void 0 ? { quality: qgridOptions.quality } : {},
710
+ ...qgridOptions.background !== void 0 ? { background: qgridOptions.background } : {},
711
+ ...qgridOptions.outputFormat !== void 0 ? { outputFormat: qgridOptions.outputFormat } : {}
712
+ },
713
+ ...imageUrls.length ? { input } : {},
714
+ ...projectName ? { projectName } : {},
715
+ ...qgridOptions.tokenName !== void 0 ? { tokenName: qgridOptions.tokenName } : {},
716
+ ...qgridOptions.logger === false ? { logger: false } : {}
717
+ } })
718
+ }, {
719
+ operation: "query",
720
+ serverUrl
721
+ });
722
+ if (!response.ok) throw new Error(`qgrid ${response.status}: ${await response.text().catch(() => "")}`);
723
+ const data = await response.json();
724
+ const images = data.content?.filter((item) => item.type === "image") ?? [];
725
+ const image = images[0];
726
+ 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");
727
+ const usage = image.generation.usage;
728
+ return {
729
+ images: [image.data],
730
+ warnings,
731
+ response: {
732
+ timestamp,
733
+ modelId: image.generation.model ?? data.model,
734
+ headers: Object.fromEntries(response.headers)
735
+ },
736
+ ...usage ? { usage: {
737
+ inputTokens: usage.input_tokens,
738
+ outputTokens: usage.output_tokens,
739
+ totalTokens: usage.total_tokens
740
+ } } : {},
741
+ providerMetadata: { qgrid: { images: [{
742
+ ...data.requestLogId !== void 0 ? { requestLogId: data.requestLogId } : {},
743
+ generation: image.generation,
744
+ ...image.revisedPrompt !== null && image.revisedPrompt !== void 0 ? { revisedPrompt: image.revisedPrompt } : {}
745
+ }] } }
746
+ };
747
+ }
748
+ };
749
+ }
750
+ //#endregion
719
751
  //#region src/logger.ts
720
752
  const DEFAULT_RUN_KEY = "__qgrid_default_run__";
721
753
  const DEFAULT_STALE_RUN_TIMEOUT_MS = 1800 * 1e3;
@@ -1044,20 +1076,9 @@ function createQgridLogger(config = {}) {
1044
1076
  const threadCoordStore = /* @__PURE__ */ new Map();
1045
1077
  const CACHE_AFFINITY_NAMESPACE = "qgrid-cache-affinity-v1";
1046
1078
  const MAX_THREAD_COORDS = 1e3;
1047
- const MAX_IMAGE_INPUT_DATA_URL_CHARS = 9e6;
1048
- const MAX_IMAGE_INPUT_DATA_URL_CHARS_LABEL = "9M";
1049
- function assertImageInputFitsJsonTransport(imageUrls) {
1050
- let totalDataUrlChars = 0;
1051
- for (const url of imageUrls) {
1052
- if (!url.toLowerCase().startsWith("data:")) continue;
1053
- totalDataUrlChars += url.length;
1054
- 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 imageGeneration, for example as WebP/JPEG under ${MAX_IMAGE_INPUT_DATA_URL_CHARS_LABEL} total base64 data-url chars.`);
1055
- }
1056
- 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 imageGeneration, for example as WebP/JPEG under ${MAX_IMAGE_INPUT_DATA_URL_CHARS_LABEL} total base64 data-url chars.`);
1057
- }
1058
1079
  function warnDroppedImages(droppedImageCount) {
1059
1080
  if (droppedImageCount <= 0) return;
1060
- console.warn(`[qgrid] ${droppedImageCount} image message part(s) were ignored because providerOptions.qgrid.imageGeneration is not enabled.`);
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().`);
1061
1082
  }
1062
1083
  function serializeObjectResponseFormat(responseFormat) {
1063
1084
  const rawSchema = responseFormat?.type === "json" ? responseFormat.schema : void 0;
@@ -1102,10 +1123,6 @@ async function deriveCacheAffinityKey(modelId, sessionKey) {
1102
1123
  return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
1103
1124
  }
1104
1125
  function qgridProviderMetadata(data) {
1105
- const imageGeneration = data.content?.flatMap((item, contentIndex) => item.type === "image" && item.generation ? [{
1106
- contentIndex,
1107
- ...item.generation
1108
- }] : []);
1109
1126
  return { qgrid: {
1110
1127
  ...data.requestLogId !== void 0 ? { requestLogId: data.requestLogId } : {},
1111
1128
  model: data.model,
@@ -1116,12 +1133,12 @@ function qgridProviderMetadata(data) {
1116
1133
  costUsd: data.costUsd,
1117
1134
  costSource: data.costSource,
1118
1135
  cacheCreation5mInputTokens: data.usage.cache_creation_5m_input_tokens ?? null,
1119
- cacheCreation1hInputTokens: data.usage.cache_creation_1h_input_tokens ?? null,
1120
- ...imageGeneration?.length ? { imageGeneration } : {}
1136
+ cacheCreation1hInputTokens: data.usage.cache_creation_1h_input_tokens ?? null
1121
1137
  } };
1122
1138
  }
1123
1139
  function getQgridProviderOptions(options) {
1124
1140
  const qgridOptions = options.providerOptions?.qgrid ?? {};
1141
+ if ("imageGeneration" in qgridOptions || "imageGenerationOptions" in qgridOptions) throw new Error("qgrid: imageGeneration and imageGenerationOptions were removed from language-model options. Use generateImage with qgrid.image().");
1125
1142
  if (qgridOptions.tokenName !== void 0 && qgridOptions.tokenName.trim() === "") throw new Error("qgrid: tokenName must not be empty");
1126
1143
  return qgridOptions;
1127
1144
  }
@@ -1189,9 +1206,8 @@ function qgrid(modelId, config) {
1189
1206
  const tools = options.tools?.filter((t) => t.type === "function");
1190
1207
  const hasTools = tools && tools.length > 0;
1191
1208
  const qgridOptions = getQgridProviderOptions(options);
1192
- const imageGeneration = qgridOptions.imageGeneration;
1193
- const { prompt, system, history, input, imageUrls, droppedImageCount } = extractPromptAndHistory(options.prompt, { includeImages: imageGeneration === true });
1194
- if (!imageGeneration) warnDroppedImages(droppedImageCount);
1209
+ const { prompt, system, history, droppedImageCount } = extractPromptAndHistory(options.prompt);
1210
+ warnDroppedImages(droppedImageCount);
1195
1211
  const effectiveEffort = qgridOptions.thinking === false ? void 0 : qgridOptions.effort ?? effort;
1196
1212
  const verbosity = qgridOptions.verbosity;
1197
1213
  const reasoningSummary = qgridOptions.reasoningSummary;
@@ -1199,10 +1215,8 @@ function qgrid(modelId, config) {
1199
1215
  const timeoutMs = qgridOptions.timeoutMs;
1200
1216
  const logger = qgridOptions.logger;
1201
1217
  const tokenName = qgridOptions.tokenName;
1202
- const imageGenerationOptions = qgridOptions.imageGenerationOptions;
1203
- if (imageGeneration) assertImageInputFitsJsonTransport(imageUrls);
1204
1218
  const sessionKey = qgridOptions.sessionKey;
1205
- const cacheAffinityKey = imageGeneration ? void 0 : await deriveCacheAffinityKey(modelId, sessionKey);
1219
+ const cacheAffinityKey = await deriveCacheAffinityKey(modelId, sessionKey);
1206
1220
  const jsonSchema = serializeObjectResponseFormat(options.responseFormat);
1207
1221
  let runContext;
1208
1222
  let toolResultsPayload;
@@ -1219,7 +1233,6 @@ function qgrid(modelId, config) {
1219
1233
  headers: { "Content-Type": "application/json" },
1220
1234
  body: JSON.stringify({ args: {
1221
1235
  prompt,
1222
- ...imageGeneration && input ? { input } : {},
1223
1236
  model: modelId,
1224
1237
  system,
1225
1238
  effort: effectiveEffort,
@@ -1236,9 +1249,7 @@ function qgrid(modelId, config) {
1236
1249
  ...logger === false ? { logger: false } : {},
1237
1250
  ...tokenName ? { tokenName } : {},
1238
1251
  ...runContext ? { runContext } : {},
1239
- ...toolResultsPayload ? { toolResults: toolResultsPayload } : {},
1240
- ...imageGeneration ? { imageGeneration } : {},
1241
- ...imageGenerationOptions ? { imageGenerationOptions } : {}
1252
+ ...toolResultsPayload ? { toolResults: toolResultsPayload } : {}
1242
1253
  } }),
1243
1254
  signal: options.abortSignal,
1244
1255
  ...transport ? { dispatcher: transport.dispatcher } : {}
@@ -1263,12 +1274,6 @@ function qgrid(modelId, config) {
1263
1274
  type: "text",
1264
1275
  text: item.text
1265
1276
  });
1266
- else if (item.type === "image") content.push({
1267
- type: "file",
1268
- mediaType: item.mediaType ?? "image/png",
1269
- data: item.data,
1270
- ...item.generation ? { providerMetadata: { qgrid: { imageGeneration: item.generation } } } : {}
1271
- });
1272
1277
  else if (item.type === "tool-call") content.push({
1273
1278
  type: "tool-call",
1274
1279
  toolCallId: item.toolCallId,
@@ -1283,10 +1288,9 @@ function qgrid(modelId, config) {
1283
1288
  type: "text",
1284
1289
  text: data.text
1285
1290
  });
1286
- if (imageGeneration && finishReason.unified !== "tool-calls" && !content.some((c) => c.type === "file")) throw new Error("qgrid: imageGeneration was requested but the response contained no image — the server may be an older version that does not support image generation.");
1287
1291
  if (matchedClientRun) clientRuns.delete(matchedClientRun.key);
1288
1292
  if (finishReason.unified === "tool-calls") rememberClientRun(data.runContext, content.filter((c) => c.type === "tool-call").map((c) => c.toolCallId));
1289
- if (!imageGeneration && cacheAffinityKey && data.runContext?.threadCoord) setThreadCoord(cacheAffinityKey, data.runContext.threadCoord);
1293
+ if (cacheAffinityKey && data.runContext?.threadCoord) setThreadCoord(cacheAffinityKey, data.runContext.threadCoord);
1290
1294
  return {
1291
1295
  content,
1292
1296
  finishReason,
@@ -1307,8 +1311,7 @@ function qgrid(modelId, config) {
1307
1311
  const timeoutMs = qgridOptions.timeoutMs;
1308
1312
  const logger = qgridOptions.logger;
1309
1313
  const tokenName = qgridOptions.tokenName;
1310
- if (qgridOptions.imageGeneration) throw new Error("qgrid: imageGeneration is not supported with streamText — use generateText instead.");
1311
- const { prompt, system, history, droppedImageCount } = extractPromptAndHistory(options.prompt, { includeImages: false });
1314
+ const { prompt, system, history, droppedImageCount } = extractPromptAndHistory(options.prompt);
1312
1315
  warnDroppedImages(droppedImageCount);
1313
1316
  const sessionKey = qgridOptions.sessionKey;
1314
1317
  const cacheAffinityKey = await deriveCacheAffinityKey(modelId, sessionKey);
@@ -1491,5 +1494,6 @@ function qgrid(modelId, config) {
1491
1494
  }
1492
1495
  };
1493
1496
  }
1497
+ qgrid.image = createQgridImage;
1494
1498
  //#endregion
1495
1499
  export { createQgridLogger, qgrid as default, qgrid };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cartanova/qgrid-ai-sdk",
3
- "version": "2.10.5",
4
- "description": "AI SDK LanguageModelV3 provider for qgrid",
3
+ "version": "2.10.6",
4
+ "description": "AI SDK LanguageModelV3 and ImageModelV3 provider for qgrid",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/cartanova-ai/qgrid"