@cartanova/qgrid-ai-sdk 2.12.0 → 2.14.0
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 +32 -23
- package/README.md +32 -23
- package/dist/index.d.ts +20 -22
- package/dist/index.js +150 -208
- package/package.json +13 -10
package/README.ko.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](./README.md) · **한국어**
|
|
4
4
|
|
|
5
|
-
AI SDK
|
|
5
|
+
[qgrid](https://github.com/cartanova-ai/Qgrid)용 AI SDK 7 custom provider입니다. AI SDK 7이 받는 `LanguageModelV3` / `ImageModelV3` 명세를 구현합니다.
|
|
6
6
|
|
|
7
7
|
**기존 AI SDK 코드 변경 없이, `model` 한 줄만 바꾸면 구독 토큰 풀링(토큰 N개 × concurrent permit) + request log 대시보드를 사용할 수 있습니다.**
|
|
8
8
|
|
|
@@ -24,7 +24,7 @@ AI SDK v6 custom `LanguageModelV3` / `ImageModelV3` provider for [qgrid](https:/
|
|
|
24
24
|
const { text } = await generateText({
|
|
25
25
|
model: google("gemini-3-flash"),
|
|
26
26
|
prompt: "복잡한 질문",
|
|
27
|
-
+
|
|
27
|
+
+ telemetry: createQgridLogger({ serverUrl: "http://localhost:44900" }),
|
|
28
28
|
});
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -34,7 +34,7 @@ AI SDK v6 custom `LanguageModelV3` / `ImageModelV3` provider for [qgrid](https:/
|
|
|
34
34
|
pnpm add @cartanova/qgrid-ai-sdk
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Peer dependencies: `ai@^
|
|
37
|
+
Peer dependencies: `ai@^7.0.0`, `@ai-sdk/provider@^4.0.0`
|
|
38
38
|
|
|
39
39
|
## 빠른 시작
|
|
40
40
|
|
|
@@ -64,7 +64,7 @@ import { qgrid } from "@cartanova/qgrid-ai-sdk";
|
|
|
64
64
|
|
|
65
65
|
const { text } = await generateText({
|
|
66
66
|
model: qgrid("openai/gpt-6-luna"),
|
|
67
|
-
|
|
67
|
+
instructions: "당신은 학술 논문 요약가입니다.",
|
|
68
68
|
prompt: paperText,
|
|
69
69
|
});
|
|
70
70
|
```
|
|
@@ -80,7 +80,7 @@ import { z } from "zod";
|
|
|
80
80
|
|
|
81
81
|
const { output } = await generateText({
|
|
82
82
|
model: qgrid("openai/gpt-5.6-terra"),
|
|
83
|
-
|
|
83
|
+
instructions: "논문 메타데이터를 추출해주세요.",
|
|
84
84
|
prompt: paperText,
|
|
85
85
|
output: Output.object({
|
|
86
86
|
schema: z.object({
|
|
@@ -118,7 +118,7 @@ for await (const chunk of textStream) {
|
|
|
118
118
|
### Tool Calling
|
|
119
119
|
|
|
120
120
|
```typescript
|
|
121
|
-
import { generateText,
|
|
121
|
+
import { generateText, isStepCount, tool } from "ai";
|
|
122
122
|
import { qgrid } from "@cartanova/qgrid-ai-sdk";
|
|
123
123
|
import { z } from "zod";
|
|
124
124
|
|
|
@@ -134,7 +134,7 @@ const { text } = await generateText({
|
|
|
134
134
|
},
|
|
135
135
|
}),
|
|
136
136
|
},
|
|
137
|
-
stopWhen:
|
|
137
|
+
stopWhen: isStepCount(3),
|
|
138
138
|
});
|
|
139
139
|
```
|
|
140
140
|
|
|
@@ -146,7 +146,7 @@ tool-call 턴 뒤에도 계속 진행해 모델의 최종 응답을 받습니다
|
|
|
146
146
|
`tools`와 `Output.object`를 함께 사용할 수도 있습니다.
|
|
147
147
|
|
|
148
148
|
```typescript
|
|
149
|
-
import { generateText, Output,
|
|
149
|
+
import { generateText, Output, isStepCount, tool } from "ai";
|
|
150
150
|
import { qgrid } from "@cartanova/qgrid-ai-sdk";
|
|
151
151
|
import { z } from "zod";
|
|
152
152
|
|
|
@@ -160,7 +160,7 @@ const { output } = await generateText({
|
|
|
160
160
|
execute: async ({ city }) => ({ city, temperature: 22 }),
|
|
161
161
|
}),
|
|
162
162
|
},
|
|
163
|
-
stopWhen:
|
|
163
|
+
stopWhen: isStepCount(3),
|
|
164
164
|
output: Output.object({
|
|
165
165
|
schema: z.object({
|
|
166
166
|
city: z.string(),
|
|
@@ -345,10 +345,10 @@ qgrid는 `grok/*` 모델을 qgrid 대시보드에 등록한 xAI 구독 토큰(
|
|
|
345
345
|
| Factory | 모델 ID | AI SDK 함수 |
|
|
346
346
|
| --- | --- | --- |
|
|
347
347
|
| `qgrid(id)` | `grok/grok-4.7`(카탈로그 기본 모델), `grok/grok-4.7-build-fast`, `grok/grok-4.6`, `grok/grok-4.5`, 그 밖의 `grok/<id>` | `generateText`, `streamText` |
|
|
348
|
-
| `qgrid.image(id)` | `grok/grok-imagine-image`, `grok/grok-imagine-image-2.0
|
|
348
|
+
| `qgrid.image(id)` | `grok/grok-imagine-image`, `grok/grok-imagine-image-2.0` | `generateImage` |
|
|
349
349
|
| `qgrid.video(id)` | `grok/grok-imagine-video`, `grok/grok-imagine-video-1.5`, `grok/grok-imagine-video-1.5-lite` | `experimental_generateVideo` |
|
|
350
|
-
| `qgrid.transcription(id)` | `grok/grok-voice-transcribe-2.0` | `
|
|
351
|
-
| `qgrid.speech(id)` | `grok/grok-tts`(ID는 request log 라벨로만 쓰임) | `
|
|
350
|
+
| `qgrid.transcription(id)` | `grok/grok-voice-transcribe-2.0` | `transcribe` |
|
|
351
|
+
| `qgrid.speech(id)` | `grok/grok-tts`(ID는 request log 라벨로만 쓰임) | `generateSpeech` |
|
|
352
352
|
|
|
353
353
|
qgrid는 카탈로그에 없는 language model ID를 거부하지 않고 그대로 전달합니다(OpenAI와 같음). 판단은
|
|
354
354
|
subscription backend가 하며 다른 모델로 응답할 수 있으므로, 실제로 실행된 모델은 request log의 응답 모델(또는
|
|
@@ -403,16 +403,19 @@ Grok language model은 user 이미지 part(https URL 또는 inline bytes)를 버
|
|
|
403
403
|
|
|
404
404
|
### Grok 이미지
|
|
405
405
|
|
|
406
|
+
생성과 편집에는 `grok-imagine-image-2.0`을 사용합니다. `quality`는 `low`, `medium`, `auto`이며,
|
|
407
|
+
생략하면 `auto`입니다(현재 생성은 low, 편집은 medium).
|
|
408
|
+
|
|
406
409
|
```typescript
|
|
407
410
|
import { generateImage } from "ai";
|
|
408
411
|
import { qgrid } from "@cartanova/qgrid-ai-sdk";
|
|
409
412
|
|
|
410
413
|
const { images, providerMetadata } = await generateImage({
|
|
411
|
-
model: qgrid.image("grok/grok-imagine-image"),
|
|
414
|
+
model: qgrid.image("grok/grok-imagine-image-2.0"),
|
|
412
415
|
prompt: "A lighthouse at dusk, oil painting",
|
|
413
416
|
n: 2,
|
|
414
417
|
aspectRatio: "16:9",
|
|
415
|
-
providerOptions: { xai: { resolution: "2k", quality: "
|
|
418
|
+
providerOptions: { xai: { resolution: "2k", quality: "low", output_format: "jpeg" } },
|
|
416
419
|
});
|
|
417
420
|
images[0].mediaType; // "image/jpeg"
|
|
418
421
|
providerMetadata.xai?.images[0]; // revisedPrompt와 해당 호출의 costInUsdTicks
|
|
@@ -459,8 +462,8 @@ try {
|
|
|
459
462
|
```typescript
|
|
460
463
|
import { readFile } from "node:fs/promises";
|
|
461
464
|
import {
|
|
462
|
-
|
|
463
|
-
|
|
465
|
+
generateSpeech,
|
|
466
|
+
transcribe,
|
|
464
467
|
} from "ai";
|
|
465
468
|
import { qgrid } from "@cartanova/qgrid-ai-sdk";
|
|
466
469
|
|
|
@@ -482,14 +485,14 @@ const speech = await generateSpeech({
|
|
|
482
485
|
speech.audio.uint8Array;
|
|
483
486
|
```
|
|
484
487
|
|
|
485
|
-
- `
|
|
488
|
+
- `transcribe`는 bytes로 오디오 형식을 감지하고 실패하면 `audio/wav`를 씁니다. 서버는 `audio/*`가 아닌 값을 거부합니다. transcript에는 segment가 없습니다(`segments: []`).
|
|
486
489
|
- 음성 합성 텍스트는 최대 15,000자입니다. `voice`, `language`, `outputFormat`을 지정하지 않으면 서버 기본값 `ara`, `en`, `mp3`가 쓰이며, live 검증된 조합은 이것뿐입니다. 다른 voice, language(`@ai-sdk/xai`의 `auto` 포함), codec은 그대로 전달되지만 미검증입니다. 서버가 돌려준 오디오 MIME 타입은 `providerMetadata.qgrid.mediaType`에 있고, AI SDK의 `speech.audio.mediaType`은 bytes에서 추정한 값입니다.
|
|
487
490
|
|
|
488
491
|
## Telemetry Logger
|
|
489
492
|
|
|
490
|
-
서버와 AI SDK 2.10.0부터 qgrid provider의 `generateText` 결과에서는 `result.providerMetadata?.qgrid?.requestLogId`로 요청 로그 ID를 확인할 수 있습니다. `streamText`는 스트림 소비 후 `(await result.providerMetadata
|
|
493
|
+
서버와 AI SDK 2.10.0부터 qgrid provider의 `generateText` 결과에서는 `result.finalStep.providerMetadata?.qgrid?.requestLogId`로 요청 로그 ID를 확인할 수 있습니다. `streamText`는 스트림 소비 후 `(await result.finalStep).providerMetadata?.qgrid?.requestLogId`로 확인합니다. 직접 API를 호출하면 `query` 응답과 스트림 `done` 이벤트의 최상위 `requestLogId`에 같은 값이 담깁니다. `logger: false` 또는 구버전 서버에서는 이 필드가 없으며, 툴 호출 후속 단계는 같은 요청 로그 ID를 공유합니다. 이 ID는 후속 요청 연결 상태가 아니며, 아래 telemetry logger를 쓰는 외부 provider에는 이 metadata가 추가되지 않습니다.
|
|
491
494
|
|
|
492
|
-
qgrid provider가 아닌 모델(google, openai 직접 호출)에서도 같은 request log 대시보드를 사용하려면 `createQgridLogger`를 `
|
|
495
|
+
qgrid provider가 아닌 모델(google, openai 직접 호출)에서도 같은 request log 대시보드를 사용하려면 `createQgridLogger`를 `telemetry`에 넣으면 됩니다.
|
|
493
496
|
|
|
494
497
|
```typescript
|
|
495
498
|
import { generateText } from "ai";
|
|
@@ -499,7 +502,7 @@ import { createQgridLogger } from "@cartanova/qgrid-ai-sdk";
|
|
|
499
502
|
const { text } = await generateText({
|
|
500
503
|
model: google("gemini-3-flash"),
|
|
501
504
|
prompt: "안녕하세요",
|
|
502
|
-
|
|
505
|
+
telemetry: createQgridLogger({
|
|
503
506
|
serverUrl: "http://localhost:44900",
|
|
504
507
|
}),
|
|
505
508
|
});
|
|
@@ -537,7 +540,7 @@ const { text } = await generateText({
|
|
|
537
540
|
providerOptions: {
|
|
538
541
|
qgrid: { logger: false } satisfies QgridProviderOptions,
|
|
539
542
|
},
|
|
540
|
-
|
|
543
|
+
telemetry: createQgridLogger({ serverUrl: "http://localhost:44900" }),
|
|
541
544
|
});
|
|
542
545
|
```
|
|
543
546
|
|
|
@@ -698,6 +701,12 @@ QGRID_REAL_PROVIDER_ACCEPTANCE=1 QGRID_URL=http://localhost:44900 QGRID_MODEL=op
|
|
|
698
701
|
|
|
699
702
|
## 요구사항
|
|
700
703
|
|
|
701
|
-
- Node.js >=
|
|
702
|
-
- AI SDK
|
|
704
|
+
- Node.js >= 22
|
|
705
|
+
- AI SDK 7 (`ai@^7.0.0`)
|
|
703
706
|
- 실행 중인 qgrid 서버
|
|
707
|
+
|
|
708
|
+
### AI SDK 7로 업그레이드
|
|
709
|
+
|
|
710
|
+
- logger는 `experimental_telemetry` 대신 `telemetry: createQgridLogger(...)`로 넘깁니다.
|
|
711
|
+
- `metadata.qgridRunId`는 더 이상 필요 없습니다. logger가 AI SDK의 호출별 `callId`로 run을 구분하므로 logger 하나를 동시 호출에 함께 써도 됩니다.
|
|
712
|
+
- 요청 로그 ID는 `result.finalStep.providerMetadata`에서 읽습니다. AI SDK 7에서 최상위 `providerMetadata`는 deprecated alias로만 남아 있습니다.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**English** · [한국어](./README.ko.md)
|
|
4
4
|
|
|
5
|
-
AI SDK
|
|
5
|
+
AI SDK 7 custom provider for [qgrid](https://github.com/cartanova-ai/Qgrid), implementing the `LanguageModelV3` / `ImageModelV3` specification that AI SDK 7 accepts.
|
|
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
|
|
|
@@ -24,7 +24,7 @@ If you already use another provider (google, openai, ...) directly, add **a sing
|
|
|
24
24
|
const { text } = await generateText({
|
|
25
25
|
model: google("gemini-3-flash"),
|
|
26
26
|
prompt: "A complex question",
|
|
27
|
-
+
|
|
27
|
+
+ telemetry: createQgridLogger({ serverUrl: "http://localhost:44900" }),
|
|
28
28
|
});
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -34,7 +34,7 @@ If you already use another provider (google, openai, ...) directly, add **a sing
|
|
|
34
34
|
pnpm add @cartanova/qgrid-ai-sdk
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Peer dependencies: `ai@^
|
|
37
|
+
Peer dependencies: `ai@^7.0.0`, `@ai-sdk/provider@^4.0.0`
|
|
38
38
|
|
|
39
39
|
## Quick start
|
|
40
40
|
|
|
@@ -64,7 +64,7 @@ import { qgrid } from "@cartanova/qgrid-ai-sdk";
|
|
|
64
64
|
|
|
65
65
|
const { text } = await generateText({
|
|
66
66
|
model: qgrid("openai/gpt-6-luna"),
|
|
67
|
-
|
|
67
|
+
instructions: "You are an academic paper summarizer.",
|
|
68
68
|
prompt: paperText,
|
|
69
69
|
});
|
|
70
70
|
```
|
|
@@ -80,7 +80,7 @@ import { z } from "zod";
|
|
|
80
80
|
|
|
81
81
|
const { output } = await generateText({
|
|
82
82
|
model: qgrid("openai/gpt-5.6-terra"),
|
|
83
|
-
|
|
83
|
+
instructions: "Extract the paper metadata.",
|
|
84
84
|
prompt: paperText,
|
|
85
85
|
output: Output.object({
|
|
86
86
|
schema: z.object({
|
|
@@ -126,7 +126,7 @@ for await (const chunk of textStream) {
|
|
|
126
126
|
### Tool Calling
|
|
127
127
|
|
|
128
128
|
```typescript
|
|
129
|
-
import { generateText,
|
|
129
|
+
import { generateText, isStepCount, tool } from "ai";
|
|
130
130
|
import { qgrid } from "@cartanova/qgrid-ai-sdk";
|
|
131
131
|
import { z } from "zod";
|
|
132
132
|
|
|
@@ -142,7 +142,7 @@ const { text } = await generateText({
|
|
|
142
142
|
},
|
|
143
143
|
}),
|
|
144
144
|
},
|
|
145
|
-
stopWhen:
|
|
145
|
+
stopWhen: isStepCount(3),
|
|
146
146
|
});
|
|
147
147
|
```
|
|
148
148
|
|
|
@@ -154,7 +154,7 @@ tool-call step and asks the model for the final response.
|
|
|
154
154
|
Tools can be combined with `Output.object`:
|
|
155
155
|
|
|
156
156
|
```typescript
|
|
157
|
-
import { generateText, Output,
|
|
157
|
+
import { generateText, Output, isStepCount, tool } from "ai";
|
|
158
158
|
import { qgrid } from "@cartanova/qgrid-ai-sdk";
|
|
159
159
|
import { z } from "zod";
|
|
160
160
|
|
|
@@ -168,7 +168,7 @@ const { output } = await generateText({
|
|
|
168
168
|
execute: async ({ city }) => ({ city, temperature: 22 }),
|
|
169
169
|
}),
|
|
170
170
|
},
|
|
171
|
-
stopWhen:
|
|
171
|
+
stopWhen: isStepCount(3),
|
|
172
172
|
output: Output.object({
|
|
173
173
|
schema: z.object({
|
|
174
174
|
city: z.string(),
|
|
@@ -355,10 +355,10 @@ Grok model accepts the same factory config as the other models: `serverUrl` (def
|
|
|
355
355
|
| Factory | Model ids | AI SDK function |
|
|
356
356
|
| --- | --- | --- |
|
|
357
357
|
| `qgrid(id)` | `grok/grok-4.7` (default catalog model), `grok/grok-4.7-build-fast`, `grok/grok-4.6`, `grok/grok-4.5`, any other `grok/<id>` | `generateText`, `streamText` |
|
|
358
|
-
| `qgrid.image(id)` | `grok/grok-imagine-image`, `grok/grok-imagine-image-2.0
|
|
358
|
+
| `qgrid.image(id)` | `grok/grok-imagine-image`, `grok/grok-imagine-image-2.0` | `generateImage` |
|
|
359
359
|
| `qgrid.video(id)` | `grok/grok-imagine-video`, `grok/grok-imagine-video-1.5`, `grok/grok-imagine-video-1.5-lite` | `experimental_generateVideo` |
|
|
360
|
-
| `qgrid.transcription(id)` | `grok/grok-voice-transcribe-2.0` | `
|
|
361
|
-
| `qgrid.speech(id)` | `grok/grok-tts` (the id only labels the request log) | `
|
|
360
|
+
| `qgrid.transcription(id)` | `grok/grok-voice-transcribe-2.0` | `transcribe` |
|
|
361
|
+
| `qgrid.speech(id)` | `grok/grok-tts` (the id only labels the request log) | `generateSpeech` |
|
|
362
362
|
|
|
363
363
|
qgrid forwards a language-model id that is not in its catalog unchanged instead of rejecting it,
|
|
364
364
|
as it does for OpenAI; the subscription backend decides. It may serve a different model, so check
|
|
@@ -414,16 +414,19 @@ error. Compress the input, or pass an https image URL where the option accepts o
|
|
|
414
414
|
|
|
415
415
|
### Grok images
|
|
416
416
|
|
|
417
|
+
Use `grok-imagine-image-2.0` for generation and editing. Its `quality` values are `low`, `medium`,
|
|
418
|
+
and `auto`; omission uses `auto` (currently low for generation, medium for editing).
|
|
419
|
+
|
|
417
420
|
```typescript
|
|
418
421
|
import { generateImage } from "ai";
|
|
419
422
|
import { qgrid } from "@cartanova/qgrid-ai-sdk";
|
|
420
423
|
|
|
421
424
|
const { images, providerMetadata } = await generateImage({
|
|
422
|
-
model: qgrid.image("grok/grok-imagine-image"),
|
|
425
|
+
model: qgrid.image("grok/grok-imagine-image-2.0"),
|
|
423
426
|
prompt: "A lighthouse at dusk, oil painting",
|
|
424
427
|
n: 2,
|
|
425
428
|
aspectRatio: "16:9",
|
|
426
|
-
providerOptions: { xai: { resolution: "2k", quality: "
|
|
429
|
+
providerOptions: { xai: { resolution: "2k", quality: "low", output_format: "jpeg" } },
|
|
427
430
|
});
|
|
428
431
|
images[0].mediaType; // "image/jpeg"
|
|
429
432
|
providerMetadata.xai?.images[0]; // revisedPrompt and the call's costInUsdTicks
|
|
@@ -471,8 +474,8 @@ try {
|
|
|
471
474
|
```typescript
|
|
472
475
|
import { readFile } from "node:fs/promises";
|
|
473
476
|
import {
|
|
474
|
-
|
|
475
|
-
|
|
477
|
+
generateSpeech,
|
|
478
|
+
transcribe,
|
|
476
479
|
} from "ai";
|
|
477
480
|
import { qgrid } from "@cartanova/qgrid-ai-sdk";
|
|
478
481
|
|
|
@@ -494,14 +497,14 @@ const speech = await generateSpeech({
|
|
|
494
497
|
speech.audio.uint8Array;
|
|
495
498
|
```
|
|
496
499
|
|
|
497
|
-
- `
|
|
500
|
+
- `transcribe` detects the audio type from its bytes and falls back to `audio/wav`; the server rejects anything that is not `audio/*`. Transcripts carry no segments (`segments: []`).
|
|
498
501
|
- Speech text is limited to 15,000 characters. Unset `voice`, `language` and `outputFormat` use the server defaults `ara`, `en` and `mp3`, the only combination verified live. Other voices, languages (including `@ai-sdk/xai`'s `auto`) and codecs are passed through but unverified. The server's audio MIME type is in `providerMetadata.qgrid.mediaType`; AI SDK's `speech.audio.mediaType` is sniffed from the bytes.
|
|
499
502
|
|
|
500
503
|
## Telemetry Logger
|
|
501
504
|
|
|
502
|
-
With qgrid server and SDK 2.10.0+, read the parent request-log ID from `result.providerMetadata?.qgrid?.requestLogId` after `generateText`. For `streamText`, consume the stream first, then read `(await result.providerMetadata
|
|
505
|
+
With qgrid server and SDK 2.10.0+, read the parent request-log ID from `result.finalStep.providerMetadata?.qgrid?.requestLogId` after `generateText`. For `streamText`, consume the stream first, then read `(await result.finalStep).providerMetadata?.qgrid?.requestLogId`. Raw API query responses and stream `done` events expose the same ID as top-level `requestLogId`. The field is omitted with `logger: false` or an older server. Tool-run steps share the parent ID; it is not continuation state. This metadata applies to the qgrid provider, not external providers using the telemetry logger below.
|
|
503
506
|
|
|
504
|
-
To use the same request log dashboard with models that don't go through the qgrid provider (direct google/openai calls), pass `createQgridLogger` to `
|
|
507
|
+
To use the same request log dashboard with models that don't go through the qgrid provider (direct google/openai calls), pass `createQgridLogger` to `telemetry`.
|
|
505
508
|
|
|
506
509
|
```typescript
|
|
507
510
|
import { generateText } from "ai";
|
|
@@ -511,7 +514,7 @@ import { createQgridLogger } from "@cartanova/qgrid-ai-sdk";
|
|
|
511
514
|
const { text } = await generateText({
|
|
512
515
|
model: google("gemini-3-flash"),
|
|
513
516
|
prompt: "Hello",
|
|
514
|
-
|
|
517
|
+
telemetry: createQgridLogger({
|
|
515
518
|
serverUrl: "http://localhost:44900",
|
|
516
519
|
}),
|
|
517
520
|
});
|
|
@@ -550,7 +553,7 @@ const { text } = await generateText({
|
|
|
550
553
|
providerOptions: {
|
|
551
554
|
qgrid: { logger: false } satisfies QgridProviderOptions,
|
|
552
555
|
},
|
|
553
|
-
|
|
556
|
+
telemetry: createQgridLogger({ serverUrl: "http://localhost:44900" }),
|
|
554
557
|
});
|
|
555
558
|
```
|
|
556
559
|
|
|
@@ -714,6 +717,12 @@ Keep new tests focused on observable contracts or concrete regressions. Preserve
|
|
|
714
717
|
|
|
715
718
|
## Requirements
|
|
716
719
|
|
|
717
|
-
- Node.js >=
|
|
718
|
-
- AI SDK
|
|
720
|
+
- Node.js >= 22
|
|
721
|
+
- AI SDK 7 (`ai@^7.0.0`)
|
|
719
722
|
- A running qgrid server
|
|
723
|
+
|
|
724
|
+
### Upgrading to AI SDK 7
|
|
725
|
+
|
|
726
|
+
- Pass the logger as `telemetry: createQgridLogger(...)` instead of `experimental_telemetry`.
|
|
727
|
+
- Drop `metadata.qgridRunId`. The logger keys each run by AI SDK's per-call `callId`, so one logger can serve concurrent calls.
|
|
728
|
+
- Read the request-log ID from `result.finalStep.providerMetadata`. AI SDK 7 keeps the top-level `providerMetadata` only as a deprecated alias.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { Experimental_VideoModelV3, Experimental_VideoModelV3VideoData, ImageModelV3, LanguageModelV3, SharedV3ProviderMetadata, SpeechModelV3, TranscriptionModelV3 } from "@ai-sdk/provider";
|
|
3
|
-
import {
|
|
3
|
+
import { TelemetryOptions } from "ai";
|
|
4
4
|
//#region src/grok-options.d.ts
|
|
5
5
|
/** Effort values qgrid forwards to the Grok subscription backend (`reasoning.effort`). */
|
|
6
6
|
type QgridGrokEffort = "low" | "medium" | "high" | "xhigh";
|
|
@@ -9,7 +9,7 @@ type XaiOptionsIssue = {
|
|
|
9
9
|
message: string;
|
|
10
10
|
};
|
|
11
11
|
/** Thrown when `providerOptions.xai` has an invalid shape or value type. */
|
|
12
|
-
declare class QgridXaiOptionsError extends Error {
|
|
12
|
+
export declare class QgridXaiOptionsError extends Error {
|
|
13
13
|
readonly issues: XaiOptionsIssue[];
|
|
14
14
|
constructor(issues: XaiOptionsIssue[]);
|
|
15
15
|
}
|
|
@@ -36,7 +36,7 @@ type QgridCommonProviderConfig = {
|
|
|
36
36
|
projectName?: string;
|
|
37
37
|
};
|
|
38
38
|
/** Codex Images request ID, not a guaranteed serving version; does not select a chat model. */
|
|
39
|
-
type QgridImageModel = "openai/gpt-image-2" | "grok/grok-imagine-image" | "grok/grok-imagine-image-2.0"
|
|
39
|
+
type QgridImageModel = "openai/gpt-image-2" | "grok/grok-imagine-image" | "grok/grok-imagine-image-2.0";
|
|
40
40
|
/** Grok video generation model ids (xAI subscription; not yet wired to a provider factory). */
|
|
41
41
|
type QgridVideoModel = "grok/grok-imagine-video" | "grok/grok-imagine-video-1.5" | "grok/grok-imagine-video-1.5-lite";
|
|
42
42
|
/** Grok speech-to-text model ids. */
|
|
@@ -138,10 +138,10 @@ type QgridGrokModel = "grok/grok-4.7" | "grok/grok-4.7-build-fast" | "grok/grok-
|
|
|
138
138
|
*/
|
|
139
139
|
declare const qgridXaiProviderOptionsSchema: z.ZodObject<{
|
|
140
140
|
reasoningEffort: z.ZodOptional<z.ZodEnum<{
|
|
141
|
-
none: "none";
|
|
142
141
|
low: "low";
|
|
143
142
|
medium: "medium";
|
|
144
143
|
high: "high";
|
|
144
|
+
none: "none";
|
|
145
145
|
xhigh: "xhigh";
|
|
146
146
|
}>>;
|
|
147
147
|
maxTurns: z.ZodOptional<z.ZodNumber>;
|
|
@@ -200,29 +200,27 @@ type QgridLoggerConfig = {
|
|
|
200
200
|
*/
|
|
201
201
|
tokenName?: string;
|
|
202
202
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* failures before a final step can otherwise leave request logs running.
|
|
203
|
+
* Safety-net timeout for runs that start but never report an end, error, or
|
|
204
|
+
* abort, for example a streamText result whose stream is never consumed.
|
|
206
205
|
*
|
|
207
206
|
* Set to 0 to disable. Defaults to 30 minutes, or the AI SDK total timeout
|
|
208
207
|
* plus a short grace period when one is provided.
|
|
209
208
|
*/
|
|
210
209
|
staleRunTimeoutMs?: number;
|
|
211
210
|
/**
|
|
212
|
-
* Receives qgrid logging failures.
|
|
213
|
-
*
|
|
214
|
-
* lifecycle events can be attributed to the correct run.
|
|
211
|
+
* Receives qgrid logging failures. One logger can serve concurrent AI SDK
|
|
212
|
+
* calls; each call is logged as its own run.
|
|
215
213
|
*/
|
|
216
214
|
onLogError?: (error: Error) => void;
|
|
217
215
|
};
|
|
218
216
|
//#endregion
|
|
219
217
|
//#region src/image-model.d.ts
|
|
220
218
|
/** Server-side Fastify body limit (`sonamu.config.ts` `bodyLimit`). Larger bodies get a 413. */
|
|
221
|
-
declare const QGRID_MAX_REQUEST_BODY_BYTES: number;
|
|
219
|
+
export declare const QGRID_MAX_REQUEST_BODY_BYTES: number;
|
|
222
220
|
/** `config` for `qgrid.image/video/transcription/speech`; same fields as the language model. */
|
|
223
221
|
type QgridMediaProviderConfig = QgridImageProviderConfig;
|
|
224
222
|
/** Non-2xx response from a qgrid media endpoint. */
|
|
225
|
-
declare class QgridMediaHttpError extends Error {
|
|
223
|
+
export declare class QgridMediaHttpError extends Error {
|
|
226
224
|
readonly status: number;
|
|
227
225
|
constructor(message: string, status: number);
|
|
228
226
|
}
|
|
@@ -285,12 +283,12 @@ type QgridXaiVideoProviderOptions = {
|
|
|
285
283
|
* Raised when the poll budget runs out. The job keeps running on xAI; pass `jobHandle` to
|
|
286
284
|
* `resumeQgridVideo` (with the same `projectName`) to keep waiting.
|
|
287
285
|
*/
|
|
288
|
-
declare class QgridVideoTimeoutError extends Error {
|
|
286
|
+
export declare class QgridVideoTimeoutError extends Error {
|
|
289
287
|
readonly jobHandle: string;
|
|
290
288
|
constructor(jobHandle: string, timeoutMs: number);
|
|
291
289
|
}
|
|
292
290
|
/** The upstream job finished as failed (including content-policy blocks). */
|
|
293
|
-
declare class QgridVideoGenerationError extends Error {
|
|
291
|
+
export declare class QgridVideoGenerationError extends Error {
|
|
294
292
|
readonly jobHandle: string;
|
|
295
293
|
constructor(jobHandle: string, detail: string | undefined);
|
|
296
294
|
}
|
|
@@ -315,28 +313,28 @@ type QgridVideoResult = {
|
|
|
315
313
|
* AI SDK v3 video models expose only `doGenerate` (no `doStart` / `doStatus`), so resuming is
|
|
316
314
|
* a qgrid helper rather than an `experimental_generateVideo` option.
|
|
317
315
|
*/
|
|
318
|
-
declare function resumeQgridVideo(jobHandle: string, options?: QgridVideoPollOptions & QgridMediaProviderConfig & {
|
|
316
|
+
export declare function resumeQgridVideo(jobHandle: string, options?: QgridVideoPollOptions & QgridMediaProviderConfig & {
|
|
319
317
|
modelId?: QgridVideoModel;
|
|
320
318
|
}): Promise<QgridVideoResult>;
|
|
321
319
|
declare function createQgridVideo(modelId: QgridVideoModel, config?: QgridMediaProviderConfig): Experimental_VideoModelV3;
|
|
322
320
|
//#endregion
|
|
323
321
|
//#region src/logger.d.ts
|
|
324
|
-
declare function createQgridLogger(config?: QgridLoggerConfig):
|
|
322
|
+
export declare function createQgridLogger(config?: QgridLoggerConfig): TelemetryOptions;
|
|
325
323
|
//#endregion
|
|
326
324
|
//#region src/index.d.ts
|
|
327
325
|
/** `qgrid()` config for Grok models; `defaultEffort` uses the Grok catalog effort vocabulary. */
|
|
328
|
-
type QgridGrokProviderConfig = Omit<QgridOpenAIProviderConfig, "defaultEffort"> & {
|
|
326
|
+
export type QgridGrokProviderConfig = Omit<QgridOpenAIProviderConfig, "defaultEffort"> & {
|
|
329
327
|
/** Omitted: the server applies the model's catalog default effort. */
|
|
330
328
|
defaultEffort?: QgridGrokEffort;
|
|
331
329
|
};
|
|
332
|
-
declare function qgrid(modelId: QgridOpenAIModel, config?: QgridOpenAIProviderConfig): LanguageModelV3;
|
|
333
|
-
declare function qgrid(modelId: QgridAnthropicModel, config?: QgridAnthropicProviderConfig): LanguageModelV3;
|
|
334
|
-
declare function qgrid(modelId: QgridGrokModel, config?: QgridGrokProviderConfig): LanguageModelV3;
|
|
335
|
-
declare namespace qgrid {
|
|
330
|
+
export declare function qgrid(modelId: QgridOpenAIModel, config?: QgridOpenAIProviderConfig): LanguageModelV3;
|
|
331
|
+
export declare function qgrid(modelId: QgridAnthropicModel, config?: QgridAnthropicProviderConfig): LanguageModelV3;
|
|
332
|
+
export declare function qgrid(modelId: QgridGrokModel, config?: QgridGrokProviderConfig): LanguageModelV3;
|
|
333
|
+
export declare namespace qgrid {
|
|
336
334
|
var image: typeof createQgridImage;
|
|
337
335
|
var video: typeof createQgridVideo;
|
|
338
336
|
var transcription: typeof createQgridTranscription;
|
|
339
337
|
var speech: typeof createQgridSpeech;
|
|
340
338
|
}
|
|
341
339
|
//#endregion
|
|
342
|
-
export {
|
|
340
|
+
export { type QgridAnthropicEffort, type QgridAnthropicModel, type QgridAnthropicProviderConfig, type QgridAnthropicProviderOptions, type QgridGrokEffort, type QgridGrokMediaProviderOptions, type QgridGrokModel, type QgridImageGenerationMetadata, type QgridImageModel, type QgridImageProviderConfig, type QgridImageProviderOptions, type QgridLoggerConfig, type QgridMediaProviderConfig, type QgridOpenAIEffort, type QgridOpenAIModel, type QgridOpenAIProviderConfig, type QgridOpenAIProviderOptions, type QgridProviderConfig, type QgridProviderOptions, type QgridSpeechModel, type QgridSupportedModel, type QgridTranscriptionModel, type QgridVideoModel, type QgridVideoPollOptions, type QgridVideoResult, type QgridXaiImageProviderOptions, type QgridXaiProviderOptions, type QgridXaiTranscriptionProviderOptions, type QgridXaiVideoProviderOptions, qgrid as default };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { Agent } from "undici";
|
|
2
|
+
import { Agent, fetch as fetch$1 } from "undici";
|
|
3
3
|
//#region src/envelope-stream-parser.ts
|
|
4
4
|
const ESCAPE_MAP = /* @__PURE__ */ new Map([
|
|
5
5
|
["\"", "\""],
|
|
@@ -520,19 +520,21 @@ function mapXaiProviderOptions(options, modelId) {
|
|
|
520
520
|
}
|
|
521
521
|
}
|
|
522
522
|
const effort = values.reasoningEffort;
|
|
523
|
-
if (effort !== void 0)
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
523
|
+
if (effort !== void 0) {
|
|
524
|
+
if (effort === "none") {
|
|
525
|
+
result.warnings.push({
|
|
526
|
+
key: "reasoningEffort",
|
|
527
|
+
reason: "reasoningEffort \"none\" is not supported; the qgrid Grok route always reasons"
|
|
528
|
+
});
|
|
529
|
+
result.rejected.push("reasoningEffort");
|
|
530
|
+
} else if (isGrokEffort(effort) && grokEffortsForModel(modelId).includes(effort)) result.request.effort = effort;
|
|
531
|
+
else {
|
|
532
|
+
result.warnings.push({
|
|
533
|
+
key: "reasoningEffort",
|
|
534
|
+
reason: `reasoningEffort "${effort}" is not supported by ${modelId}`
|
|
535
|
+
});
|
|
536
|
+
result.rejected.push("reasoningEffort");
|
|
537
|
+
}
|
|
536
538
|
}
|
|
537
539
|
if (values.promptCacheKey !== void 0) result.request.promptCacheKey = values.promptCacheKey;
|
|
538
540
|
return result;
|
|
@@ -567,9 +569,19 @@ async function closeQgridRequestTransport(transport) {
|
|
|
567
569
|
if (!transport) return;
|
|
568
570
|
await transport.dispatcher.close().catch(() => void 0);
|
|
569
571
|
}
|
|
570
|
-
async function qgridFetch(
|
|
572
|
+
async function qgridFetch(target, init, context) {
|
|
571
573
|
try {
|
|
572
|
-
|
|
574
|
+
const url = new URL(target);
|
|
575
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new TypeError(`qgrid server URL must use http or https, not ${url.protocol}`);
|
|
576
|
+
const href = url.href;
|
|
577
|
+
if (init.dispatcher) return await fetch$1(href, init);
|
|
578
|
+
const { method, body, signal, headers } = init;
|
|
579
|
+
return await fetch(href, {
|
|
580
|
+
method,
|
|
581
|
+
body,
|
|
582
|
+
signal,
|
|
583
|
+
headers
|
|
584
|
+
});
|
|
573
585
|
} catch (error) {
|
|
574
586
|
throw toQgridTransportError(error, context);
|
|
575
587
|
}
|
|
@@ -630,7 +642,7 @@ function serverOrigin(serverUrl) {
|
|
|
630
642
|
//#endregion
|
|
631
643
|
//#region src/qgrid.constant.ts
|
|
632
644
|
const QGRID_PROVIDER_NAME = "qgrid";
|
|
633
|
-
const THREAD_COORD_TTL_MS =
|
|
645
|
+
const THREAD_COORD_TTL_MS = 6e5;
|
|
634
646
|
const TEXT_STREAM_ID_PREFIX = "text";
|
|
635
647
|
const TOP_LEVEL_SCHEMA_WARNING = "qgrid server-enforced structured output requires \"object\". Falling back to client-side parsing.";
|
|
636
648
|
const PENDING_TOOL_RESULTS_WARNING = "[qgrid] tool results did not match any pending qgrid run; sending request without runContext";
|
|
@@ -709,6 +721,13 @@ function extractToolResultsFromHistory(messages) {
|
|
|
709
721
|
});
|
|
710
722
|
return out;
|
|
711
723
|
}
|
|
724
|
+
function parseSseData(raw) {
|
|
725
|
+
try {
|
|
726
|
+
return JSON.parse(raw);
|
|
727
|
+
} catch {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
712
731
|
async function* parseSSE(body, signal) {
|
|
713
732
|
const reader = body.getReader();
|
|
714
733
|
const decoder = new TextDecoder();
|
|
@@ -728,13 +747,11 @@ async function* parseSSE(body, signal) {
|
|
|
728
747
|
buffer = lines.pop() ?? "";
|
|
729
748
|
for (const line of lines) if (line.startsWith("event: ")) eventType = line.slice(7).trim();
|
|
730
749
|
else if (line.startsWith("data: ")) {
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
};
|
|
737
|
-
} catch {}
|
|
750
|
+
const data = parseSseData(line.slice(6));
|
|
751
|
+
if (data !== void 0) yield {
|
|
752
|
+
type: eventType || "message",
|
|
753
|
+
data
|
|
754
|
+
};
|
|
738
755
|
eventType = "";
|
|
739
756
|
}
|
|
740
757
|
}
|
|
@@ -925,11 +942,9 @@ function getErrorMessage(cause) {
|
|
|
925
942
|
return "unknown error";
|
|
926
943
|
}
|
|
927
944
|
}
|
|
928
|
-
function extractUserPrompt(
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
for (let i = messageList.length - 1; i >= 0; i--) {
|
|
932
|
-
const msg = messageList[i];
|
|
945
|
+
function extractUserPrompt(messages) {
|
|
946
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
947
|
+
const msg = messages[i];
|
|
933
948
|
if (msg?.role === "user") return extractTextContent(msg.content);
|
|
934
949
|
}
|
|
935
950
|
return "";
|
|
@@ -974,7 +989,7 @@ function imageInputUrl(file) {
|
|
|
974
989
|
return `data:${file.mediaType};base64,${data}`;
|
|
975
990
|
}
|
|
976
991
|
/** Server-side Fastify body limit (`sonamu.config.ts` `bodyLimit`). Larger bodies get a 413. */
|
|
977
|
-
const QGRID_MAX_REQUEST_BODY_BYTES =
|
|
992
|
+
const QGRID_MAX_REQUEST_BODY_BYTES = 10485760;
|
|
978
993
|
function resolveMediaConfig(config) {
|
|
979
994
|
return {
|
|
980
995
|
serverUrl: config?.serverUrl ?? process.env.QGRID_URL ?? "http://localhost:44900",
|
|
@@ -993,7 +1008,7 @@ function formatMiB(bytes) {
|
|
|
993
1008
|
* inline images of one request together. Derived from the 10 MiB body limit: base64 grows bytes by
|
|
994
1009
|
* 4/3, so 7 MiB decoded is about 9.3 MiB encoded, leaving room for the rest of the JSON body.
|
|
995
1010
|
*/
|
|
996
|
-
const QGRID_MAX_INLINE_MEDIA_BYTES =
|
|
1011
|
+
const QGRID_MAX_INLINE_MEDIA_BYTES = 7340032;
|
|
997
1012
|
function base64DecodedBytes(data) {
|
|
998
1013
|
const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
|
|
999
1014
|
return Math.floor(data.length * 3 / 4) - padding;
|
|
@@ -1493,14 +1508,16 @@ function createQgridSpeech(modelId, config) {
|
|
|
1493
1508
|
const xai = parseXaiMediaOptions(options.providerOptions?.xai, xaiSpeechOptionsSchema, /* @__PURE__ */ new Set(), LABEL$2);
|
|
1494
1509
|
const warnings = [];
|
|
1495
1510
|
let format;
|
|
1496
|
-
if (options.outputFormat !== void 0)
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1511
|
+
if (options.outputFormat !== void 0) {
|
|
1512
|
+
if (isSpeechFormat(options.outputFormat)) format = options.outputFormat;
|
|
1513
|
+
else {
|
|
1514
|
+
format = "mp3";
|
|
1515
|
+
warnings.push({
|
|
1516
|
+
type: "unsupported",
|
|
1517
|
+
feature: "outputFormat",
|
|
1518
|
+
details: `Unsupported output format: ${options.outputFormat}. Using mp3 instead.`
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1504
1521
|
}
|
|
1505
1522
|
if (options.instructions !== void 0) warnings.push({
|
|
1506
1523
|
type: "unsupported",
|
|
@@ -1896,6 +1913,18 @@ function createQgridVideo(modelId, config) {
|
|
|
1896
1913
|
type: "unsupported",
|
|
1897
1914
|
feature: "seed"
|
|
1898
1915
|
});
|
|
1916
|
+
if (options.frameImages !== void 0) warnings.push({
|
|
1917
|
+
type: "unsupported",
|
|
1918
|
+
feature: "frameImages"
|
|
1919
|
+
});
|
|
1920
|
+
if (options.inputReferences !== void 0) warnings.push({
|
|
1921
|
+
type: "unsupported",
|
|
1922
|
+
feature: "inputReferences"
|
|
1923
|
+
});
|
|
1924
|
+
if (options.generateAudio !== void 0) warnings.push({
|
|
1925
|
+
type: "unsupported",
|
|
1926
|
+
feature: "generateAudio"
|
|
1927
|
+
});
|
|
1899
1928
|
let resolution = xaiOptions?.resolution ?? void 0;
|
|
1900
1929
|
if (resolution === void 0 && options.resolution !== void 0) {
|
|
1901
1930
|
resolution = RESOLUTION_MAP.get(options.resolution);
|
|
@@ -1906,12 +1935,14 @@ function createQgridVideo(modelId, config) {
|
|
|
1906
1935
|
});
|
|
1907
1936
|
}
|
|
1908
1937
|
let lastFrame;
|
|
1909
|
-
if (xaiOptions?.lastFrame !== void 0)
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1938
|
+
if (xaiOptions?.lastFrame !== void 0) {
|
|
1939
|
+
if (modelId === LAST_FRAME_MODEL) lastFrame = toImageRefUrl(xaiOptions.lastFrame, LABEL);
|
|
1940
|
+
else warnings.push({
|
|
1941
|
+
type: "unsupported",
|
|
1942
|
+
feature: "providerOptions.xai.lastFrame",
|
|
1943
|
+
details: `xAI only supports last_frame with "${LAST_FRAME_MODEL}". The last frame was ignored.`
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1915
1946
|
const image = options.image === void 0 ? void 0 : toImageRef(options.image, LABEL);
|
|
1916
1947
|
assertInlineMediaFits([
|
|
1917
1948
|
image,
|
|
@@ -1957,11 +1988,21 @@ function createQgridVideo(modelId, config) {
|
|
|
1957
1988
|
}
|
|
1958
1989
|
//#endregion
|
|
1959
1990
|
//#region src/logger.ts
|
|
1960
|
-
const
|
|
1961
|
-
const DEFAULT_STALE_RUN_TIMEOUT_MS = 1800 * 1e3;
|
|
1991
|
+
const DEFAULT_STALE_RUN_TIMEOUT_MS = 18e5;
|
|
1962
1992
|
const STALE_RUN_GRACE_MS = 5e3;
|
|
1963
|
-
const
|
|
1964
|
-
const
|
|
1993
|
+
const STALE_RUN_ERROR = "AI SDK generation reported no end, error, or abort before the stale-run timeout";
|
|
1994
|
+
const TEXT_OPERATIONS = /* @__PURE__ */ new Set(["ai.generateText", "ai.streamText"]);
|
|
1995
|
+
const errorEventSchema = z.object({
|
|
1996
|
+
callId: z.string(),
|
|
1997
|
+
error: z.unknown()
|
|
1998
|
+
});
|
|
1999
|
+
const abortErrorSchema = z.object({ name: z.enum(["AbortError", "TimeoutError"]) });
|
|
2000
|
+
function isTextStart(event) {
|
|
2001
|
+
return TEXT_OPERATIONS.has(event.operationId);
|
|
2002
|
+
}
|
|
2003
|
+
function isTextEnd(event) {
|
|
2004
|
+
return "finalStep" in event;
|
|
2005
|
+
}
|
|
1965
2006
|
function isParseableJson(value) {
|
|
1966
2007
|
try {
|
|
1967
2008
|
JSON.parse(value ?? "");
|
|
@@ -1975,46 +2016,25 @@ function fullModelName(provider, modelId) {
|
|
|
1975
2016
|
const prefix = `${provider.split(".", 1)[0] ?? provider}/`;
|
|
1976
2017
|
return modelId.startsWith(prefix) ? modelId : `${prefix}${modelId}`;
|
|
1977
2018
|
}
|
|
1978
|
-
function
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
const existing = timers.get(k);
|
|
1986
|
-
if (existing) clearTimeout(existing);
|
|
1987
|
-
const t = setTimeout(() => {
|
|
1988
|
-
keys.delete(k);
|
|
1989
|
-
timers.delete(k);
|
|
1990
|
-
}, ttlMs);
|
|
1991
|
-
t.unref?.();
|
|
1992
|
-
timers.set(k, t);
|
|
1993
|
-
},
|
|
1994
|
-
remove(k) {
|
|
1995
|
-
keys.delete(k);
|
|
1996
|
-
const t = timers.get(k);
|
|
1997
|
-
if (t) clearTimeout(t);
|
|
1998
|
-
timers.delete(k);
|
|
1999
|
-
}
|
|
2000
|
-
};
|
|
2019
|
+
function staleRunTimeout(config, timeout) {
|
|
2020
|
+
if (config.staleRunTimeoutMs !== void 0) return config.staleRunTimeoutMs;
|
|
2021
|
+
if (timeout instanceof Object) {
|
|
2022
|
+
const totalMs = timeout.totalMs;
|
|
2023
|
+
return totalMs !== void 0 && totalMs > 0 ? totalMs + STALE_RUN_GRACE_MS : DEFAULT_STALE_RUN_TIMEOUT_MS;
|
|
2024
|
+
}
|
|
2025
|
+
return timeout !== void 0 && timeout > 0 ? timeout + STALE_RUN_GRACE_MS : DEFAULT_STALE_RUN_TIMEOUT_MS;
|
|
2001
2026
|
}
|
|
2002
2027
|
function createQgridLogger(config = {}) {
|
|
2003
2028
|
const serverUrl = config.serverUrl ?? process.env.QGRID_URL ?? "http://localhost:44900";
|
|
2004
2029
|
const projectName = config.projectName ?? process.env.QGRID_PROJECT_NAME;
|
|
2005
2030
|
const onLogError = config.onLogError ?? ((e) => console.warn(`[qgrid-logger] ${e.message}`));
|
|
2006
2031
|
const runs = /* @__PURE__ */ new Map();
|
|
2007
|
-
const
|
|
2008
|
-
|
|
2009
|
-
const suppressedRuns = timedKeySet();
|
|
2010
|
-
const quarantined = timedKeySet();
|
|
2011
|
-
const finalizeRun = async (runKey, result) => {
|
|
2012
|
-
const run = runs.get(runKey);
|
|
2032
|
+
const finalizeRun = async (callId, result) => {
|
|
2033
|
+
const run = runs.get(callId);
|
|
2013
2034
|
if (!run || run.finishing) return;
|
|
2014
2035
|
run.finishing = true;
|
|
2015
|
-
runs.delete(
|
|
2036
|
+
runs.delete(callId);
|
|
2016
2037
|
if (run.watchdog) clearTimeout(run.watchdog);
|
|
2017
|
-
run.cleanupAbortListener?.();
|
|
2018
2038
|
for (const pending of run.pendingToolCalls) run.pendingSteps.push(appendStep(serverUrl, {
|
|
2019
2039
|
requestLogId: run.requestLogId,
|
|
2020
2040
|
stepIndex: pending.stepIndex,
|
|
@@ -2046,147 +2066,63 @@ function createQgridLogger(config = {}) {
|
|
|
2046
2066
|
if (result.errorMessage) finishInput.errorMessage = result.errorMessage;
|
|
2047
2067
|
await finishRun(serverUrl, finishInput).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e))));
|
|
2048
2068
|
};
|
|
2049
|
-
let autoRunIdCounter = 0;
|
|
2050
|
-
const resolveRunKey = (qgridRunId, functionId) => {
|
|
2051
|
-
if (qgridRunId !== void 0) return `qgridRunId:${qgridRunId}`;
|
|
2052
|
-
if (functionId) return `functionId:${functionId}`;
|
|
2053
|
-
return DEFAULT_RUN_KEY;
|
|
2054
|
-
};
|
|
2055
2069
|
return {
|
|
2056
2070
|
isEnabled: true,
|
|
2057
2071
|
integrations: [{
|
|
2058
2072
|
async onStart(event) {
|
|
2059
|
-
if (!event
|
|
2060
|
-
|
|
2061
|
-
if (event.
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
return;
|
|
2065
|
-
}
|
|
2066
|
-
if (runs.has(runKey) || starting.has(runKey) || suppressedRuns.has(runKey)) {
|
|
2067
|
-
const reservation = starting.get(runKey);
|
|
2068
|
-
if (reservation) {
|
|
2069
|
-
reservation.errorMessage = OVERLAPPING_RUN_ERROR;
|
|
2070
|
-
starting.delete(runKey);
|
|
2071
|
-
}
|
|
2072
|
-
suppressedRuns.remove(runKey);
|
|
2073
|
-
quarantined.add(runKey, keyTtl);
|
|
2074
|
-
const finalizing = finalizeRun(runKey, {
|
|
2075
|
-
status: "error",
|
|
2076
|
-
errorMessage: OVERLAPPING_RUN_ERROR
|
|
2077
|
-
});
|
|
2078
|
-
onLogError(/* @__PURE__ */ new Error(OVERLAPPING_RUN_ERROR));
|
|
2079
|
-
await finalizing;
|
|
2080
|
-
return;
|
|
2081
|
-
}
|
|
2082
|
-
if (event.providerOptions?.qgrid?.logger === false) {
|
|
2083
|
-
suppressedRuns.add(runKey, keyTtl);
|
|
2084
|
-
return;
|
|
2085
|
-
}
|
|
2086
|
-
const requestedModelName = fullModelName(event.model.provider, event.model.modelId) ?? event.model.modelId;
|
|
2087
|
-
const reservation = {
|
|
2088
|
-
requestedModelName,
|
|
2089
|
-
startTime: Date.now()
|
|
2090
|
-
};
|
|
2091
|
-
starting.set(runKey, reservation);
|
|
2073
|
+
if (!isTextStart(event)) return;
|
|
2074
|
+
if (event.provider === "qgrid") return;
|
|
2075
|
+
if (event.providerOptions?.qgrid?.logger === false) return;
|
|
2076
|
+
const requestedModelName = fullModelName(event.provider, event.modelId) ?? event.modelId;
|
|
2077
|
+
const startTime = Date.now();
|
|
2092
2078
|
try {
|
|
2093
|
-
const messages = event.messages ?? (Array.isArray(event.prompt) ? event.prompt : void 0);
|
|
2094
|
-
const history = serializeHistory(messages);
|
|
2095
2079
|
const responseFormat = event.output ? await event.output.responseFormat : void 0;
|
|
2096
2080
|
const isStructured = responseFormat?.type === "json";
|
|
2097
2081
|
const jsonSchema = isStructured && responseFormat.schema !== void 0 ? JSON.stringify(responseFormat.schema) ?? null : null;
|
|
2098
2082
|
const result = await createRun(serverUrl, {
|
|
2099
|
-
userPrompt: extractUserPrompt(event.
|
|
2100
|
-
systemPrompt: extractSystemPrompt(event.
|
|
2083
|
+
userPrompt: extractUserPrompt(event.messages),
|
|
2084
|
+
systemPrompt: extractSystemPrompt(event.instructions),
|
|
2101
2085
|
modelName: requestedModelName,
|
|
2102
2086
|
projectName,
|
|
2103
|
-
history,
|
|
2087
|
+
history: serializeHistory(event.messages),
|
|
2104
2088
|
isStructured,
|
|
2105
2089
|
jsonSchema
|
|
2106
2090
|
});
|
|
2107
|
-
|
|
2108
|
-
if (starting.get(runKey) === reservation) starting.delete(runKey);
|
|
2109
|
-
await finishRun(serverUrl, {
|
|
2110
|
-
requestLogId: result.requestLogId,
|
|
2111
|
-
status: "error",
|
|
2112
|
-
tokenName: config.tokenName ?? "external",
|
|
2113
|
-
requestedModelName: reservation.requestedModelName,
|
|
2114
|
-
totalInputTokens: 0,
|
|
2115
|
-
totalOutputTokens: 0,
|
|
2116
|
-
totalCacheReadTokens: 0,
|
|
2117
|
-
totalCacheCreationTokens: 0,
|
|
2118
|
-
totalDurationMs: Date.now() - reservation.startTime,
|
|
2119
|
-
errorMessage: reservation.errorMessage ?? OVERLAPPING_RUN_ERROR
|
|
2120
|
-
}).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e))));
|
|
2121
|
-
return;
|
|
2122
|
-
}
|
|
2123
|
-
let watchdogTimeout = DEFAULT_STALE_RUN_TIMEOUT_MS;
|
|
2124
|
-
const timeout = event.timeout;
|
|
2125
|
-
if (config.staleRunTimeoutMs !== void 0) watchdogTimeout = config.staleRunTimeoutMs;
|
|
2126
|
-
else if (timeout instanceof Object) {
|
|
2127
|
-
const totalMs = timeout.totalMs;
|
|
2128
|
-
if (totalMs !== void 0 && totalMs > 0) watchdogTimeout = totalMs + STALE_RUN_GRACE_MS;
|
|
2129
|
-
} else if (timeout !== void 0 && timeout > 0) watchdogTimeout = timeout + STALE_RUN_GRACE_MS;
|
|
2091
|
+
const watchdogTimeout = staleRunTimeout(config, event.timeout);
|
|
2130
2092
|
let watchdog;
|
|
2131
2093
|
if (watchdogTimeout > 0) {
|
|
2132
2094
|
watchdog = setTimeout(() => {
|
|
2133
|
-
finalizeRun(
|
|
2095
|
+
finalizeRun(event.callId, {
|
|
2134
2096
|
status: "error",
|
|
2135
|
-
errorMessage:
|
|
2097
|
+
errorMessage: STALE_RUN_ERROR
|
|
2136
2098
|
});
|
|
2137
2099
|
}, watchdogTimeout);
|
|
2138
2100
|
watchdog.unref?.();
|
|
2139
2101
|
}
|
|
2140
|
-
|
|
2141
|
-
const signal = event.abortSignal;
|
|
2142
|
-
if (signal) {
|
|
2143
|
-
const onAbort = () => {
|
|
2144
|
-
finalizeRun(runKey, {
|
|
2145
|
-
status: "aborted",
|
|
2146
|
-
errorMessage: getErrorMessage(signal.reason)
|
|
2147
|
-
});
|
|
2148
|
-
};
|
|
2149
|
-
if (signal.aborted) queueMicrotask(onAbort);
|
|
2150
|
-
else {
|
|
2151
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
2152
|
-
cleanupAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
starting.delete(runKey);
|
|
2156
|
-
runs.set(runKey, {
|
|
2102
|
+
runs.set(event.callId, {
|
|
2157
2103
|
requestLogId: result.requestLogId,
|
|
2158
2104
|
requestedModelName,
|
|
2159
2105
|
isStructured,
|
|
2160
2106
|
pendingSteps: [],
|
|
2161
2107
|
pendingToolCalls: [],
|
|
2162
|
-
startTime
|
|
2108
|
+
startTime,
|
|
2163
2109
|
toolDurations: /* @__PURE__ */ new Map(),
|
|
2164
2110
|
watchdog,
|
|
2165
|
-
cleanupAbortListener,
|
|
2166
2111
|
finishing: false
|
|
2167
2112
|
});
|
|
2168
2113
|
} catch (e) {
|
|
2169
|
-
if (starting.get(runKey) === reservation) starting.delete(runKey);
|
|
2170
2114
|
onLogError(e instanceof Error ? e : new Error(String(e)));
|
|
2171
2115
|
}
|
|
2172
2116
|
},
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
const runKey = resolveRunKey(qgridRunIdSchema.safeParse(event.metadata?.qgridRunId).data, event.functionId);
|
|
2176
|
-
if (suppressedRuns.has(runKey) || quarantined.has(runKey)) return;
|
|
2177
|
-
const run = runs.get(runKey);
|
|
2178
|
-
if (!run) return;
|
|
2179
|
-
run.toolDurations.set(event.toolCall.toolCallId, Math.round(event.durationMs));
|
|
2117
|
+
onToolExecutionEnd(event) {
|
|
2118
|
+
runs.get(event.callId)?.toolDurations.set(event.toolCall.toolCallId, Math.round(event.toolExecutionMs));
|
|
2180
2119
|
},
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
const runKey = resolveRunKey(qgridRunIdSchema.safeParse(event.metadata?.qgridRunId).data, event.functionId);
|
|
2184
|
-
if (suppressedRuns.has(runKey) || quarantined.has(runKey)) return;
|
|
2185
|
-
const run = runs.get(runKey);
|
|
2120
|
+
onStepEnd(event) {
|
|
2121
|
+
const run = runs.get(event.callId);
|
|
2186
2122
|
if (!run) return;
|
|
2187
2123
|
const { content, usage, reasoningText, finishReason, stepNumber } = event;
|
|
2188
|
-
const requestedModelName = fullModelName(event.model
|
|
2189
|
-
const modelName = fullModelName(event.model
|
|
2124
|
+
const requestedModelName = fullModelName(event.model.provider, event.model.modelId) ?? run.requestedModelName;
|
|
2125
|
+
const modelName = fullModelName(event.model.provider, event.response.modelId);
|
|
2190
2126
|
const remainingPending = [];
|
|
2191
2127
|
for (const pending of run.pendingToolCalls) {
|
|
2192
2128
|
const tr = content.find((p) => p.type === "tool-result" && p.toolCallId === pending.toolCallId);
|
|
@@ -2204,7 +2140,7 @@ function createQgridLogger(config = {}) {
|
|
|
2204
2140
|
requestedModelName: pending.requestedModelName,
|
|
2205
2141
|
toolResult: tr && "output" in tr ? loggedPayloadText.parse(tr.output) : void 0,
|
|
2206
2142
|
toolDurationMs: run.toolDurations.get(pending.toolCallId),
|
|
2207
|
-
error: te && "error" in te ?
|
|
2143
|
+
error: te && "error" in te ? getErrorMessage(te.error) : void 0
|
|
2208
2144
|
}).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e)))));
|
|
2209
2145
|
run.toolDurations.delete(pending.toolCallId);
|
|
2210
2146
|
} else remainingPending.push(pending);
|
|
@@ -2241,7 +2177,7 @@ function createQgridLogger(config = {}) {
|
|
|
2241
2177
|
requestedModelName,
|
|
2242
2178
|
toolResult: tr && "output" in tr ? loggedPayloadText.parse(tr.output) : void 0,
|
|
2243
2179
|
toolDurationMs: run.toolDurations.get(tc.toolCallId),
|
|
2244
|
-
error: te && "error" in te ?
|
|
2180
|
+
error: te && "error" in te ? getErrorMessage(te.error) : void 0
|
|
2245
2181
|
}).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e)))));
|
|
2246
2182
|
run.toolDurations.delete(tc.toolCallId);
|
|
2247
2183
|
} else run.pendingToolCalls.push({
|
|
@@ -2255,28 +2191,31 @@ function createQgridLogger(config = {}) {
|
|
|
2255
2191
|
});
|
|
2256
2192
|
}
|
|
2257
2193
|
},
|
|
2258
|
-
async
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
return;
|
|
2264
|
-
}
|
|
2265
|
-
if (quarantined.has(runKey)) return;
|
|
2266
|
-
const run = runs.get(runKey);
|
|
2267
|
-
if (!run) return;
|
|
2268
|
-
const status = event.finishReason === "error" ? "error" : "succeeded";
|
|
2269
|
-
const requestedModelName = fullModelName(event.model?.provider, event.model?.modelId) ?? run.requestedModelName;
|
|
2270
|
-
const modelName = fullModelName(event.model?.provider, event.response?.modelId);
|
|
2271
|
-
const result = {
|
|
2272
|
-
status,
|
|
2194
|
+
async onEnd(event) {
|
|
2195
|
+
const run = runs.get(event.callId);
|
|
2196
|
+
if (!run || !isTextEnd(event)) return;
|
|
2197
|
+
await finalizeRun(event.callId, {
|
|
2198
|
+
status: event.finishReason === "error" ? "error" : "succeeded",
|
|
2273
2199
|
response: event.text,
|
|
2274
|
-
totalUsage: event.
|
|
2275
|
-
modelName,
|
|
2276
|
-
requestedModelName
|
|
2277
|
-
};
|
|
2278
|
-
|
|
2279
|
-
|
|
2200
|
+
totalUsage: event.usage,
|
|
2201
|
+
modelName: fullModelName(event.model.provider, event.finalStep.response.modelId),
|
|
2202
|
+
requestedModelName: fullModelName(event.model.provider, event.model.modelId) ?? run.requestedModelName
|
|
2203
|
+
});
|
|
2204
|
+
},
|
|
2205
|
+
async onError(event) {
|
|
2206
|
+
const parsed = errorEventSchema.safeParse(event);
|
|
2207
|
+
if (!parsed.success) return;
|
|
2208
|
+
const { callId, error } = parsed.data;
|
|
2209
|
+
await finalizeRun(callId, {
|
|
2210
|
+
status: abortErrorSchema.safeParse(error).success ? "aborted" : "error",
|
|
2211
|
+
errorMessage: getErrorMessage(error)
|
|
2212
|
+
});
|
|
2213
|
+
},
|
|
2214
|
+
async onAbort(event) {
|
|
2215
|
+
await finalizeRun(event.callId, {
|
|
2216
|
+
status: "aborted",
|
|
2217
|
+
errorMessage: getErrorMessage(event.reason)
|
|
2218
|
+
});
|
|
2280
2219
|
}
|
|
2281
2220
|
}]
|
|
2282
2221
|
};
|
|
@@ -2665,13 +2604,16 @@ function qgrid(modelId, config) {
|
|
|
2665
2604
|
if (tokenName) args.tokenName = tokenName;
|
|
2666
2605
|
if (runContext) args.runContext = runContext;
|
|
2667
2606
|
if (toolResultsPayload) args.toolResults = toolResultsPayload;
|
|
2668
|
-
const
|
|
2607
|
+
const request = {
|
|
2669
2608
|
method: "POST",
|
|
2670
2609
|
headers: { "Content-Type": "application/json" },
|
|
2671
2610
|
body: JSON.stringify({ args }),
|
|
2672
2611
|
signal: options.abortSignal
|
|
2673
2612
|
};
|
|
2674
|
-
|
|
2613
|
+
const init = transport ? {
|
|
2614
|
+
...request,
|
|
2615
|
+
dispatcher: transport.dispatcher
|
|
2616
|
+
} : request;
|
|
2675
2617
|
const data = await qgridFetch(`${serverUrl}/api/qgrid/query`, init, {
|
|
2676
2618
|
operation: "query",
|
|
2677
2619
|
serverUrl,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cartanova/qgrid-ai-sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.0",
|
|
4
4
|
"description": "AI SDK LanguageModelV3 and ImageModelV3 provider for qgrid",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -20,21 +20,24 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"undici": "^
|
|
23
|
+
"undici": "^7.30.1"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@ai-sdk/provider": "^
|
|
27
|
-
"ai": "^
|
|
28
|
-
"tsdown": "^0.
|
|
29
|
-
"typescript": "
|
|
30
|
-
"vitest": "^
|
|
31
|
-
"zod": "4.
|
|
26
|
+
"@ai-sdk/provider": "^4.0.0",
|
|
27
|
+
"ai": "^7.0.0",
|
|
28
|
+
"tsdown": "^0.23.1",
|
|
29
|
+
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
30
|
+
"vitest": "^5.0.5",
|
|
31
|
+
"zod": "^4.6.7"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@ai-sdk/provider": "^
|
|
35
|
-
"ai": "^
|
|
34
|
+
"@ai-sdk/provider": "^4.0.0",
|
|
35
|
+
"ai": "^7.0.0",
|
|
36
36
|
"zod": "^3.25.76 || ^4.1.8"
|
|
37
37
|
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=22"
|
|
40
|
+
},
|
|
38
41
|
"scripts": {
|
|
39
42
|
"build": "tsdown",
|
|
40
43
|
"test": "vitest run",
|