@cartanova/qgrid-ai-sdk 2.10.4 → 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 +50 -35
- package/README.md +50 -60
- package/dist/index.d.ts +26 -21
- package/dist/index.js +178 -174
- package/package.json +2 -2
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,51 +268,68 @@ const { text } = await generateText({
|
|
|
270
268
|
|
|
271
269
|
### Image Generation
|
|
272
270
|
|
|
273
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
292
|
+
image.uint8Array; // 실제 반환된 PNG bytes
|
|
293
|
+
usage.inputTokens; // 이미지 모델 입력 토큰 (채팅 모델 usage가 아님)
|
|
294
|
+
providerMetadata.qgrid?.images[0]; // requestLogId, generation: 생성 조건과 세부 usage
|
|
288
295
|
```
|
|
289
296
|
|
|
290
|
-
|
|
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
|
|
294
|
-
model: qgrid("openai/gpt-
|
|
295
|
-
|
|
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
|
-
-
|
|
309
|
-
-
|
|
310
|
-
-
|
|
311
|
-
- 이미지
|
|
312
|
-
- `
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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만 지원합니다.
|
|
318
333
|
|
|
319
334
|
## Telemetry Logger
|
|
320
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,77 +277,69 @@ const { text } = await generateText({
|
|
|
279
277
|
|
|
280
278
|
### Image Generation
|
|
281
279
|
|
|
282
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
305
|
-
model: qgrid("openai/gpt-
|
|
306
|
-
|
|
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
|
-
- 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).
|
|
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
|
-
- Transparent image output metadata is available in `result.providerMetadata.qgrid.imageGeneration` (an array containing `contentIndex`, `model`, `route`, `size`, `quality`, `background`, and optional image `usage`). Per-file metadata also appears on `result.content` file parts, not on `result.files[]`. Request options are hints: a live `1024x1024`/`high` request returned `1254x1254`/`medium`. qgrid reports the actual PNG dimensions and does not resize it.
|
|
325
|
-
- 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.
|
|
326
|
-
- Explicit image options are recorded as `requestedOptions` in the synthetic image tool log, separately from `pricingAssumption`. When size/quality are omitted, the `1536x1024`/`medium` pricing defaults do not establish actual output dimensions or quality.
|
|
327
|
-
|
|
328
|
-
Transparent generation example (requires a server and SDK containing this feature):
|
|
329
|
-
|
|
330
|
-
```typescript
|
|
331
|
-
import { generateText } from "ai";
|
|
332
|
-
import { qgrid, type QgridProviderOptions } from "@cartanova/qgrid-ai-sdk";
|
|
333
|
-
|
|
334
|
-
const result = await generateText({
|
|
335
|
-
model: qgrid("openai/gpt-6.1-sol", { projectName: "deti" }),
|
|
336
|
-
prompt: "An isolated character. Everything outside the character must be transparent, with no backdrop or checkerboard.",
|
|
337
|
-
providerOptions: {
|
|
338
|
-
qgrid: {
|
|
339
|
-
imageGeneration: true,
|
|
340
|
-
imageGenerationOptions: {
|
|
341
|
-
background: "transparent",
|
|
342
|
-
quality: "high",
|
|
343
|
-
size: "1024x1536",
|
|
344
|
-
},
|
|
345
|
-
} satisfies QgridProviderOptions,
|
|
346
|
-
},
|
|
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] },
|
|
347
320
|
});
|
|
348
|
-
const png = result.files[0];
|
|
349
|
-
const observed = result.providerMetadata?.qgrid?.imageGeneration;
|
|
350
321
|
```
|
|
351
322
|
|
|
352
|
-
|
|
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`.
|
|
353
343
|
|
|
354
344
|
## Telemetry Logger
|
|
355
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,23 +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
|
-
* quality/size cost estimates use the GPT Image 2 public calculator table.
|
|
79
|
-
*/
|
|
80
|
-
imageGenerationOptions?: {
|
|
81
|
-
/** Responses output format. Defaults to PNG; transparent output only supports PNG. */
|
|
82
|
-
outputFormat?: "png" | "jpeg" | "webp";
|
|
83
|
-
quality?: "low" | "medium" | "high";
|
|
84
|
-
size?: "1024x1024" | "1024x1536" | "1536x1024";
|
|
85
|
-
/** Transparent uses Codex standalone Images and validates PNG alpha. See README for limits. */
|
|
86
|
-
background?: "auto" | "opaque" | "transparent";
|
|
87
|
-
};
|
|
88
86
|
};
|
|
89
87
|
/** `providerOptions.qgrid` — Anthropic(Claude Code) 모델용. */
|
|
90
88
|
type QgridAnthropicProviderOptions = QgridCommonProviderOptions & {
|
|
@@ -107,8 +105,9 @@ type QgridProviderOptions = QgridOpenAIProviderOptions | QgridAnthropicProviderO
|
|
|
107
105
|
type QgridSupportedModel = "openai/gpt-6-astra" | "openai/gpt-6.1-sol" | "openai/gpt-6-sol" | "openai/gpt-6-luna" | "openai/gpt-5.6-sol" | "openai/gpt-5.6-terra" | "openai/gpt-5.6-luna" | "openai/gpt-5.5" | "openai/gpt-5.4" | "openai/gpt-5.2" | "openai/gpt-5.4-mini" | "openai/gpt-5.3-codex" | "openai/gpt-5.3-codex-spark" | "anthropic/claude-fable-5-1" | "anthropic/claude-fable-5" | "anthropic/claude-haiku-4-5" | "anthropic/claude-sonnet-4" | "anthropic/claude-sonnet-4-5" | "anthropic/claude-sonnet-4-6" | "anthropic/claude-sonnet-4-7" | "anthropic/claude-sonnet-5" | "anthropic/claude-opus-4" | "anthropic/claude-opus-4-1" | "anthropic/claude-opus-4-5" | "anthropic/claude-opus-4-6" | "anthropic/claude-opus-4-7" | "anthropic/claude-opus-4-8" | "anthropic/claude-opus-5" | "anthropic/claude-opus-5-5" | "anthropic/claude-sonnet-5-5" | "anthropic/claude-haiku-5-5";
|
|
108
106
|
/** Observed image response values. Usage is response-level and appears on the first image only. */
|
|
109
107
|
type QgridImageGenerationMetadata = {
|
|
110
|
-
route: "codex-images";
|
|
111
|
-
|
|
108
|
+
route: "codex-images" | "codex-responses";
|
|
109
|
+
/** Present only when reported or known from the standalone route. */
|
|
110
|
+
model?: string;
|
|
112
111
|
background?: string;
|
|
113
112
|
quality?: string;
|
|
114
113
|
size?: string;
|
|
@@ -155,11 +154,17 @@ type QgridLoggerConfig = {
|
|
|
155
154
|
onLogError?: (error: Error) => void;
|
|
156
155
|
};
|
|
157
156
|
//#endregion
|
|
157
|
+
//#region src/image-model.d.ts
|
|
158
|
+
declare function createQgridImage(modelId: QgridImageModel, config?: QgridImageProviderConfig): ImageModelV3;
|
|
159
|
+
//#endregion
|
|
158
160
|
//#region src/logger.d.ts
|
|
159
161
|
declare function createQgridLogger(config?: QgridLoggerConfig): TelemetrySettings;
|
|
160
162
|
//#endregion
|
|
161
163
|
//#region src/index.d.ts
|
|
162
164
|
declare function qgrid(modelId: QgridOpenAIModel, config?: QgridOpenAIProviderConfig): LanguageModelV3;
|
|
163
165
|
declare function qgrid(modelId: QgridAnthropicModel, config?: QgridAnthropicProviderConfig): LanguageModelV3;
|
|
166
|
+
declare namespace qgrid {
|
|
167
|
+
var image: typeof createQgridImage;
|
|
168
|
+
}
|
|
164
169
|
//#endregion
|
|
165
|
-
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
481
|
-
imageUrls.push(...extracted.imageUrls);
|
|
471
|
+
const extracted = extractPromptTextParts(msg.content);
|
|
482
472
|
droppedImageCount += extracted.droppedImageCount;
|
|
483
|
-
const content =
|
|
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")
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
type
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
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
|
|
517
|
+
function extractPromptTextParts(content) {
|
|
559
518
|
if (typeof content === "string") return {
|
|
519
|
+
text: content,
|
|
560
520
|
parts: [{
|
|
561
|
-
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
|
595
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1193
|
-
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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"
|