@cartanova/qgrid-ai-sdk 2.12.0 → 2.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](./README.md) · **한국어**
4
4
 
5
- AI SDK v6 custom `LanguageModelV3` / `ImageModelV3` provider for [qgrid](https://github.com/cartanova-ai/Qgrid).
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
- + experimental_telemetry: createQgridLogger({ serverUrl: "http://localhost:44900" }),
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@^6.0.0`, `@ai-sdk/provider@^3.0.0`
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
- system: "당신은 학술 논문 요약가입니다.",
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
- system: "논문 메타데이터를 추출해주세요.",
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, stepCountIs, tool } from "ai";
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: stepCountIs(3),
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, stepCountIs, tool } from "ai";
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: stepCountIs(3),
163
+ stopWhen: isStepCount(3),
164
164
  output: Output.object({
165
165
  schema: z.object({
166
166
  city: z.string(),
@@ -347,8 +347,8 @@ qgrid는 `grok/*` 모델을 qgrid 대시보드에 등록한 xAI 구독 토큰(
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
348
  | `qgrid.image(id)` | `grok/grok-imagine-image`, `grok/grok-imagine-image-2.0`, `grok/grok-imagine-image-quality` | `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` | `experimental_transcribe` |
351
- | `qgrid.speech(id)` | `grok/grok-tts`(ID는 request log 라벨로만 쓰임) | `experimental_generateSpeech` |
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의 응답 모델(또는
@@ -459,8 +459,8 @@ try {
459
459
  ```typescript
460
460
  import { readFile } from "node:fs/promises";
461
461
  import {
462
- experimental_generateSpeech as generateSpeech,
463
- experimental_transcribe as transcribe,
462
+ generateSpeech,
463
+ transcribe,
464
464
  } from "ai";
465
465
  import { qgrid } from "@cartanova/qgrid-ai-sdk";
466
466
 
@@ -482,14 +482,14 @@ const speech = await generateSpeech({
482
482
  speech.audio.uint8Array;
483
483
  ```
484
484
 
485
- - `experimental_transcribe`는 bytes로 오디오 형식을 감지하고 실패하면 `audio/wav`를 씁니다. 서버는 `audio/*`가 아닌 값을 거부합니다. transcript에는 segment가 없습니다(`segments: []`).
485
+ - `transcribe`는 bytes로 오디오 형식을 감지하고 실패하면 `audio/wav`를 씁니다. 서버는 `audio/*`가 아닌 값을 거부합니다. transcript에는 segment가 없습니다(`segments: []`).
486
486
  - 음성 합성 텍스트는 최대 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
487
 
488
488
  ## Telemetry Logger
489
489
 
490
- 서버와 AI SDK 2.10.0부터 qgrid provider의 `generateText` 결과에서는 `result.providerMetadata?.qgrid?.requestLogId`로 요청 로그 ID를 확인할 수 있습니다. `streamText`는 스트림 소비 후 `(await result.providerMetadata)?.qgrid?.requestLogId`로 확인합니다. 직접 API를 호출하면 `query` 응답과 스트림 `done` 이벤트의 최상위 `requestLogId`에 같은 값이 담깁니다. `logger: false` 또는 구버전 서버에서는 이 필드가 없으며, 툴 호출 후속 단계는 같은 요청 로그 ID를 공유합니다. 이 ID는 후속 요청 연결 상태가 아니며, 아래 telemetry logger를 쓰는 외부 provider에는 이 metadata가 추가되지 않습니다.
490
+ 서버와 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
491
 
492
- qgrid provider가 아닌 모델(google, openai 직접 호출)에서도 같은 request log 대시보드를 사용하려면 `createQgridLogger`를 `experimental_telemetry`에 넣으면 됩니다.
492
+ qgrid provider가 아닌 모델(google, openai 직접 호출)에서도 같은 request log 대시보드를 사용하려면 `createQgridLogger`를 `telemetry`에 넣으면 됩니다.
493
493
 
494
494
  ```typescript
495
495
  import { generateText } from "ai";
@@ -499,7 +499,7 @@ import { createQgridLogger } from "@cartanova/qgrid-ai-sdk";
499
499
  const { text } = await generateText({
500
500
  model: google("gemini-3-flash"),
501
501
  prompt: "안녕하세요",
502
- experimental_telemetry: createQgridLogger({
502
+ telemetry: createQgridLogger({
503
503
  serverUrl: "http://localhost:44900",
504
504
  }),
505
505
  });
@@ -537,7 +537,7 @@ const { text } = await generateText({
537
537
  providerOptions: {
538
538
  qgrid: { logger: false } satisfies QgridProviderOptions,
539
539
  },
540
- experimental_telemetry: createQgridLogger({ serverUrl: "http://localhost:44900" }),
540
+ telemetry: createQgridLogger({ serverUrl: "http://localhost:44900" }),
541
541
  });
542
542
  ```
543
543
 
@@ -698,6 +698,12 @@ QGRID_REAL_PROVIDER_ACCEPTANCE=1 QGRID_URL=http://localhost:44900 QGRID_MODEL=op
698
698
 
699
699
  ## 요구사항
700
700
 
701
- - Node.js >= 20
702
- - AI SDK v6 (`ai@^6.0.0`)
701
+ - Node.js >= 22
702
+ - AI SDK 7 (`ai@^7.0.0`)
703
703
  - 실행 중인 qgrid 서버
704
+
705
+ ### AI SDK 7로 업그레이드
706
+
707
+ - logger는 `experimental_telemetry` 대신 `telemetry: createQgridLogger(...)`로 넘깁니다.
708
+ - `metadata.qgridRunId`는 더 이상 필요 없습니다. logger가 AI SDK의 호출별 `callId`로 run을 구분하므로 logger 하나를 동시 호출에 함께 써도 됩니다.
709
+ - 요청 로그 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 v6 custom `LanguageModelV3` / `ImageModelV3` provider for [qgrid](https://github.com/cartanova-ai/Qgrid).
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
- + experimental_telemetry: createQgridLogger({ serverUrl: "http://localhost:44900" }),
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@^6.0.0`, `@ai-sdk/provider@^3.0.0`
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
- system: "You are an academic paper summarizer.",
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
- system: "Extract the paper metadata.",
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, stepCountIs, tool } from "ai";
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: stepCountIs(3),
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, stepCountIs, tool } from "ai";
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: stepCountIs(3),
171
+ stopWhen: isStepCount(3),
172
172
  output: Output.object({
173
173
  schema: z.object({
174
174
  city: z.string(),
@@ -357,8 +357,8 @@ Grok model accepts the same factory config as the other models: `serverUrl` (def
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
358
  | `qgrid.image(id)` | `grok/grok-imagine-image`, `grok/grok-imagine-image-2.0`, `grok/grok-imagine-image-quality` | `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` | `experimental_transcribe` |
361
- | `qgrid.speech(id)` | `grok/grok-tts` (the id only labels the request log) | `experimental_generateSpeech` |
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
@@ -471,8 +471,8 @@ try {
471
471
  ```typescript
472
472
  import { readFile } from "node:fs/promises";
473
473
  import {
474
- experimental_generateSpeech as generateSpeech,
475
- experimental_transcribe as transcribe,
474
+ generateSpeech,
475
+ transcribe,
476
476
  } from "ai";
477
477
  import { qgrid } from "@cartanova/qgrid-ai-sdk";
478
478
 
@@ -494,14 +494,14 @@ const speech = await generateSpeech({
494
494
  speech.audio.uint8Array;
495
495
  ```
496
496
 
497
- - `experimental_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: []`).
497
+ - `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
498
  - 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
499
 
500
500
  ## Telemetry Logger
501
501
 
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)?.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.
502
+ 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
503
 
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 `experimental_telemetry`.
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 `telemetry`.
505
505
 
506
506
  ```typescript
507
507
  import { generateText } from "ai";
@@ -511,7 +511,7 @@ import { createQgridLogger } from "@cartanova/qgrid-ai-sdk";
511
511
  const { text } = await generateText({
512
512
  model: google("gemini-3-flash"),
513
513
  prompt: "Hello",
514
- experimental_telemetry: createQgridLogger({
514
+ telemetry: createQgridLogger({
515
515
  serverUrl: "http://localhost:44900",
516
516
  }),
517
517
  });
@@ -550,7 +550,7 @@ const { text } = await generateText({
550
550
  providerOptions: {
551
551
  qgrid: { logger: false } satisfies QgridProviderOptions,
552
552
  },
553
- experimental_telemetry: createQgridLogger({ serverUrl: "http://localhost:44900" }),
553
+ telemetry: createQgridLogger({ serverUrl: "http://localhost:44900" }),
554
554
  });
555
555
  ```
556
556
 
@@ -714,6 +714,12 @@ Keep new tests focused on observable contracts or concrete regressions. Preserve
714
714
 
715
715
  ## Requirements
716
716
 
717
- - Node.js >= 20
718
- - AI SDK v6 (`ai@^6.0.0`)
717
+ - Node.js >= 22
718
+ - AI SDK 7 (`ai@^7.0.0`)
719
719
  - A running qgrid server
720
+
721
+ ### Upgrading to AI SDK 7
722
+
723
+ - Pass the logger as `telemetry: createQgridLogger(...)` instead of `experimental_telemetry`.
724
+ - Drop `metadata.qgridRunId`. The logger keys each run by AI SDK's per-call `callId`, so one logger can serve concurrent calls.
725
+ - 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 { TelemetrySettings } from "ai";
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
  }
@@ -200,29 +200,27 @@ type QgridLoggerConfig = {
200
200
  */
201
201
  tokenName?: string;
202
202
  /**
203
- * Fallback timeout for runs that receive onStart but never receive onFinish.
204
- * AI SDK TelemetryIntegration does not expose an error hook, so provider
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. When reusing one logger integration across
213
- * overlapping AI SDK calls, pass a unique `metadata.qgridRunId` per call so
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): TelemetrySettings;
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 { QGRID_MAX_REQUEST_BODY_BYTES, type QgridAnthropicEffort, type QgridAnthropicModel, type QgridAnthropicProviderConfig, type QgridAnthropicProviderOptions, type QgridGrokEffort, type QgridGrokMediaProviderOptions, type QgridGrokModel, QgridGrokProviderConfig, type QgridImageGenerationMetadata, type QgridImageModel, type QgridImageProviderConfig, type QgridImageProviderOptions, type QgridLoggerConfig, QgridMediaHttpError, type QgridMediaProviderConfig, type QgridOpenAIEffort, type QgridOpenAIModel, type QgridOpenAIProviderConfig, type QgridOpenAIProviderOptions, type QgridProviderConfig, type QgridProviderOptions, type QgridSpeechModel, type QgridSupportedModel, type QgridTranscriptionModel, QgridVideoGenerationError, type QgridVideoModel, type QgridVideoPollOptions, type QgridVideoResult, QgridVideoTimeoutError, type QgridXaiImageProviderOptions, QgridXaiOptionsError, type QgridXaiProviderOptions, type QgridXaiTranscriptionProviderOptions, type QgridXaiVideoProviderOptions, createQgridLogger, qgrid as default, qgrid, resumeQgridVideo };
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) if (effort === "none") {
524
- result.warnings.push({
525
- key: "reasoningEffort",
526
- reason: "reasoningEffort \"none\" is not supported; the qgrid Grok route always reasons"
527
- });
528
- result.rejected.push("reasoningEffort");
529
- } else if (isGrokEffort(effort) && grokEffortsForModel(modelId).includes(effort)) result.request.effort = effort;
530
- else {
531
- result.warnings.push({
532
- key: "reasoningEffort",
533
- reason: `reasoningEffort "${effort}" is not supported by ${modelId}`
534
- });
535
- result.rejected.push("reasoningEffort");
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(input, init, context) {
572
+ async function qgridFetch(target, init, context) {
571
573
  try {
572
- return await fetch(input, init);
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 = 10 * 6e4;
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 raw = line.slice(6);
732
- try {
733
- yield {
734
- type: eventType || "message",
735
- data: JSON.parse(raw)
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(prompt, messages) {
929
- if (prompt !== void 0 && !Array.isArray(prompt)) return prompt;
930
- const messageList = messages ?? prompt ?? [];
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 = 10 * 1024 * 1024;
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 = 7 * 1024 * 1024;
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) if (isSpeechFormat(options.outputFormat)) format = options.outputFormat;
1497
- else {
1498
- format = "mp3";
1499
- warnings.push({
1500
- type: "unsupported",
1501
- feature: "outputFormat",
1502
- details: `Unsupported output format: ${options.outputFormat}. Using mp3 instead.`
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) if (modelId === LAST_FRAME_MODEL) lastFrame = toImageRefUrl(xaiOptions.lastFrame, LABEL);
1910
- else warnings.push({
1911
- type: "unsupported",
1912
- feature: "providerOptions.xai.lastFrame",
1913
- details: `xAI only supports last_frame with "${LAST_FRAME_MODEL}". The last frame was ignored.`
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 DEFAULT_RUN_KEY = "__qgrid_default_run__";
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 OVERLAPPING_RUN_ERROR = "createQgridLogger received overlapping runs for the same telemetry key. Pass a unique metadata.qgridRunId per AI SDK call or create a fresh logger integration per call.";
1964
- const qgridRunIdSchema = z.string().min(1);
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 timedKeySet() {
1979
- const keys = /* @__PURE__ */ new Set();
1980
- const timers = /* @__PURE__ */ new Map();
1981
- return {
1982
- has: (k) => keys.has(k),
1983
- add(k, ttlMs) {
1984
- keys.add(k);
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 starting = /* @__PURE__ */ new Map();
2008
- const keyTtl = config.staleRunTimeoutMs !== void 0 && config.staleRunTimeoutMs > 0 ? config.staleRunTimeoutMs : DEFAULT_STALE_RUN_TIMEOUT_MS;
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(runKey);
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.metadata?.qgridRunId && !event.functionId && event.metadata) event.metadata.qgridRunId = `auto-${++autoRunIdCounter}`;
2060
- const runKey = resolveRunKey(qgridRunIdSchema.safeParse(event.metadata?.qgridRunId).data, event.functionId);
2061
- if (event.model.provider === "qgrid") return;
2062
- if (quarantined.has(runKey)) {
2063
- onLogError(/* @__PURE__ */ new Error("createQgridLogger: telemetry key is quarantined after overlap"));
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.prompt, messages),
2100
- systemPrompt: extractSystemPrompt(event.system),
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
- if (reservation.errorMessage || quarantined.has(runKey) || starting.get(runKey) !== reservation) {
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(runKey, {
2095
+ finalizeRun(event.callId, {
2134
2096
  status: "error",
2135
- errorMessage: "AI SDK generation ended before onFinish was emitted"
2097
+ errorMessage: STALE_RUN_ERROR
2136
2098
  });
2137
2099
  }, watchdogTimeout);
2138
2100
  watchdog.unref?.();
2139
2101
  }
2140
- let cleanupAbortListener;
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: Date.now(),
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
- onToolCallFinish(event) {
2174
- if (event.model?.provider === "qgrid") return;
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
- onStepFinish(event) {
2182
- if (event.model?.provider === "qgrid") return;
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?.provider, event.model?.modelId) ?? run.requestedModelName;
2189
- const modelName = fullModelName(event.model?.provider, event.response?.modelId);
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 ? loggedPayloadText.parse(te.error) : void 0
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 ? loggedPayloadText.parse(te.error) : void 0
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 onFinish(event) {
2259
- if (event.model?.provider === "qgrid") return;
2260
- const runKey = resolveRunKey(qgridRunIdSchema.safeParse(event.metadata?.qgridRunId).data, event.functionId);
2261
- if (suppressedRuns.has(runKey)) {
2262
- suppressedRuns.remove(runKey);
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.totalUsage,
2275
- modelName,
2276
- requestedModelName
2277
- };
2278
- if (status === "error" && "error" in event) result.errorMessage = getErrorMessage(event.error);
2279
- await finalizeRun(runKey, result);
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 init = {
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
- if (transport) init.dispatcher = transport.dispatcher;
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.12.0",
3
+ "version": "2.13.1",
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": "^6.28.0"
23
+ "undici": "^7.30.1"
24
24
  },
25
25
  "devDependencies": {
26
- "@ai-sdk/provider": "^3.0.0",
27
- "ai": "^6.0.190",
28
- "tsdown": "^0.22.3",
29
- "typescript": "^5.9.3",
30
- "vitest": "^4.1.10",
31
- "zod": "4.4.3"
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": "^3.0.0",
35
- "ai": "^6.0.0",
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",