@cartanova/qgrid-ai-sdk 2.4.7 → 2.4.9

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
@@ -139,6 +139,9 @@ AI SDK가 tool 실행을 관리하고, qgrid는 각 턴의 LLM 호출만 담당
139
139
  ### Provider Options
140
140
 
141
141
  qgrid 전용 옵션은 전부 `providerOptions.qgrid` 네임스페이스로 전달합니다. (`providerOptions.openai`가 아닙니다)
142
+ AI SDK는 바깥 `providerOptions`를 범용 JSON record로 타입 선언하므로 qgrid 옵션을 자동 추론하지 못합니다.
143
+ 따라서 공개 타입 `QgridProviderOptions`를 중첩된 `qgrid` 값에 `satisfies`로 적용하세요. literal 추론을
144
+ 유지하면서 qgrid 옵션의 오타와 잘못된 값을 컴파일 타임에 잡을 수 있습니다.
142
145
 
143
146
  ```typescript
144
147
  import { generateText } from "ai";
@@ -159,6 +162,7 @@ const { text } = await generateText({
159
162
 
160
163
  | 옵션 | 값 | 적용 범위 | 설명 |
161
164
  |---|---|---|---|
165
+ | `logger` | `boolean` | 공통 | qgrid request log 저장 여부. 기본값은 `true`. `false`로 설정해도 client tool 실행과 multi-step 연결은 계속 동작 |
162
166
  | `sessionKey` | `string` | OpenAI 전용 | 멀티턴 대화 식별자. 같은 key는 같은 codex thread로 라우팅되어 prompt cache 적중 ([아래](#멀티턴-prompt-cache-sessionkey) 참조) |
163
167
  | `effort` | `"none"` \| `"minimal"` \| `"low"` \| `"medium"` \| `"high"` \| `"xhigh"` \| `"max"` | 공통 (지원 값은 모델별 상이, 예: `"max"`는 GPT-5.6+) | reasoning 모델의 추론 깊이. 기본값은 config의 `defaultEffort` (`"low"`) |
164
168
  | `verbosity` | `"low"` \| `"medium"` \| `"high"` | OpenAI 전용 | 응답 텍스트의 상세도 |
@@ -166,7 +170,7 @@ const { text } = await generateText({
166
170
  | `serviceTier` | `string` | OpenAI 전용 | OpenAI/codex service tier |
167
171
  | `imageGeneration` | `boolean` | OpenAI 전용, non-stream | codex 내장 `image_generation` tool 활성화 ([아래](#image-generation) 참조) |
168
172
  | `imageGenerationOptions` | `{ quality?, size? }` | OpenAI 전용 | 이미지 품질/크기 힌트. `quality: "low" \| "medium" \| "high"`, `size: "1024x1024" \| "1024x1536" \| "1536x1024"` (기본: `medium` / `1536x1024`) |
169
- | `fallbackModels` | `string[]` | 예약 | 향후 서버 fallback routing용 예약 필드. 현재 동작하지 않음 |
173
+ | `fallbackModels` | `string[]` | 예약 | 향후 qgrid 서버 fallback routing용 예약 필드. 현재 동작하지 않으며 Claude Code의 Fable refusal fallback과 무관 |
170
174
 
171
175
  ### 멀티턴 prompt cache (sessionKey)
172
176
 
@@ -260,6 +264,29 @@ createQgridLogger({
260
264
 
261
265
  모든 설정은 optional (serverUrl 제외). 기본값이 있으므로 `serverUrl`만 넣으면 동작합니다.
262
266
 
267
+ 특정 generation을 request log 저장에서 제외하려면 `providerOptions.qgrid.logger`를
268
+ `false`로 설정하세요. qgrid provider 호출과 `createQgridLogger`가 관찰하는 외부 provider
269
+ 호출에 모두 적용되며, tool 실행과 multi-step 연결은 계속 정상 동작합니다.
270
+
271
+ ```typescript
272
+ import { type QgridProviderOptions } from "@cartanova/qgrid-ai-sdk";
273
+
274
+ const { text } = await generateText({
275
+ model: google("gemini-3-flash"),
276
+ prompt: "이 요청은 저장하지 마",
277
+ providerOptions: {
278
+ qgrid: { logger: false } satisfies QgridProviderOptions,
279
+ },
280
+ experimental_telemetry: createQgridLogger({ serverUrl: "http://localhost:44900" }),
281
+ });
282
+ ```
283
+
284
+ 외부 provider request log의 모델 이름은 `provider/modelId` 형식으로 저장됩니다. provider가
285
+ AI SDK response metadata로 실제 serving 모델을 다르게 보고하면 step과 final log에는
286
+ 관찰된 serving 모델을 저장하고 requested model은 별도로 유지합니다. AI SDK runtime의
287
+ `response.modelId`는 변경하지 않습니다. `openai.responses`는 `openai`로,
288
+ `anthropic.messages`는 `anthropic`으로 적재하는 등 AI SDK adapter suffix는 base provider로 정규화합니다.
289
+
263
290
  ### qgrid provider와 함께 사용
264
291
 
265
292
  `qgrid()` provider는 자체 lifecycle이 있으므로 logger가 자동으로 suppress됩니다. 같은 코드에서 qgrid provider와 다른 provider를 섞어 써도 이중 기록되지 않습니다.
@@ -279,6 +306,7 @@ type QgridSupportedModel =
279
306
  | "openai/gpt-5.3-codex"
280
307
  | "openai/gpt-5.3-codex-spark"
281
308
  // Anthropic
309
+ | "anthropic/claude-fable-5"
282
310
  | "anthropic/claude-haiku-4-5"
283
311
  | "anthropic/claude-sonnet-4"
284
312
  | "anthropic/claude-sonnet-4-5"
@@ -291,6 +319,7 @@ type QgridSupportedModel =
291
319
  | "anthropic/claude-opus-4-6"
292
320
  | "anthropic/claude-opus-4-7"
293
321
  | "anthropic/claude-opus-4-8"
322
+ | "anthropic/claude-opus-5"
294
323
  ```
295
324
 
296
325
  ### GPT-5.6 사양
@@ -303,6 +332,14 @@ type QgridSupportedModel =
303
332
 
304
333
  세 모델 모두 `max` reasoning effort를 지원합니다. OpenAI native API 사양은 1.05M context / 128K 최대 출력이지만, qgrid는 codex app-server 구독 경로로 실행되어 세 모델 모두 context window가 372K(95% effective, 실사용 입력 약 353K)로 보고되며 설정으로 올릴 수 없습니다. 입력이 272K tokens를 넘으면 요청 전체에 input 2x, output 1.5x 장문 컨텍스트 할증이 적용되며, cache write는 uncached input 단가의 1.25x입니다.
305
334
 
335
+ `anthropic/claude-fable-5`는 1M context와 128K 최대 출력을 지원합니다. 1M tokens당 표준 단가는 input $10, cache read $1, 5분 cache write $12.50, 1시간 cache write $20, output $50입니다. qgrid는 Claude 응답의 5분/1시간 cache creation breakdown을 보존해 TTL별 단가를 각각 적용하며, breakdown이 없는 구버전 응답에서만 Claude Code가 subscription OAuth 경로에 자동 적용하는 1시간 TTL 단가로 fallback합니다. Fable은 adaptive thinking이 항상 켜져 있어야 하므로 qgrid는 이 모델의 adaptive thinking을 보존합니다.
336
+
337
+ `anthropic/claude-opus-5`는 기본 1M context와 128K 최대 출력을 지원합니다. 1M tokens당 단가는 input $5, cache read $0.50, 5분 cache write $6.25, 1시간 cache write $10, output $25입니다. qgrid는 Opus 5의 기본 adaptive thinking 동작을 유지하고 `effort`로 추론 깊이를 조절합니다. 따라서 `xhigh` 또는 `max` effort에서 허용되지 않는 `thinking: disabled` 조합도 만들지 않습니다.
338
+
339
+ Claude Code는 Fable의 safety refusal을 Opus 4.8로 자동 재시도할 수 있습니다. 이 경우 AI SDK 응답의 `response.modelId`와 `providerMetadata.qgrid.model`은 실제 serving 모델인 Opus를 가리킵니다. `providerMetadata.qgrid.requestedModel`은 Fable로 유지되고, `providerMetadata.qgrid.modelFallbacks`에 refusal fallback 이력이 담깁니다. 같은 metadata에서 `costSource`와 5분/1시간 cache-write 토큰 분해도 확인할 수 있습니다.
340
+
341
+ `openai/gpt-5.3-codex-spark`는 아직 token 단가가 확정·공개되지 않은 research preview입니다. 따라서 qgrid는 generic fallback 추정치를 보고하며, 이를 공식 단가로 취급하지 않습니다.
342
+
306
343
  ## 설정
307
344
 
308
345
  ```typescript
package/README.md CHANGED
@@ -139,6 +139,9 @@ The AI SDK manages tool execution; qgrid handles only each turn's LLM call.
139
139
  ### Provider Options
140
140
 
141
141
  All qgrid-specific options go under the `providerOptions.qgrid` namespace. (Not `providerOptions.openai`.)
142
+ AI SDK types the outer `providerOptions` as a generic JSON record, so apply the exported
143
+ `QgridProviderOptions` type to the nested `qgrid` value with `satisfies`. This preserves
144
+ literal inference and catches misspelled or invalid qgrid options at compile time.
142
145
 
143
146
  ```typescript
144
147
  import { generateText } from "ai";
@@ -159,6 +162,7 @@ const { text } = await generateText({
159
162
 
160
163
  | Option | Values | Applies to | Description |
161
164
  |---|---|---|---|
165
+ | `logger` | `boolean` | both providers | qgrid request logging. Defaults to `true`; `false` disables request-log persistence for this generation without disabling client tools or multi-step continuation |
162
166
  | `sessionKey` | `string` | OpenAI only | Multi-turn conversation identifier. The same key routes to the same codex thread for prompt-cache hits (see [below](#multi-turn-prompt-cache-sessionkey)) |
163
167
  | `effort` | `"none"` \| `"minimal"` \| `"low"` \| `"medium"` \| `"high"` \| `"xhigh"` \| `"max"` | both providers (supported values are model-dependent, e.g. `"max"` is GPT-5.6+) | Reasoning depth. Defaults to the config's `defaultEffort` (`"low"`) |
164
168
  | `verbosity` | `"low"` \| `"medium"` \| `"high"` | OpenAI only | Response text verbosity |
@@ -166,7 +170,7 @@ const { text } = await generateText({
166
170
  | `serviceTier` | `string` | OpenAI only | OpenAI/codex service tier |
167
171
  | `imageGeneration` | `boolean` | OpenAI only, non-stream | Enables codex's built-in `image_generation` tool (see [below](#image-generation)) |
168
172
  | `imageGenerationOptions` | `{ quality?, size? }` | OpenAI only | Image quality/size hints. `quality: "low" \| "medium" \| "high"`, `size: "1024x1024" \| "1024x1536" \| "1536x1024"` (defaults: `medium` / `1536x1024`) |
169
- | `fallbackModels` | `string[]` | reserved | Reserved for future server-side fallback routing. Not functional yet |
173
+ | `fallbackModels` | `string[]` | reserved | Reserved for future qgrid server-side fallback routing. Not functional yet and unrelated to Claude Code's Fable refusal fallback |
170
174
 
171
175
  ### Multi-turn prompt cache (sessionKey)
172
176
 
@@ -260,6 +264,29 @@ createQgridLogger({
260
264
 
261
265
  Everything except `serverUrl` is optional. With defaults in place, passing only `serverUrl` works.
262
266
 
267
+ To opt one generation out of request logging, set `providerOptions.qgrid.logger` to `false`.
268
+ The same option works for qgrid-provider calls and for external-provider calls observed by
269
+ `createQgridLogger`; tool execution and multi-step continuation still work normally.
270
+
271
+ ```typescript
272
+ import { type QgridProviderOptions } from "@cartanova/qgrid-ai-sdk";
273
+
274
+ const { text } = await generateText({
275
+ model: google("gemini-3-flash"),
276
+ prompt: "Do not persist this request",
277
+ providerOptions: {
278
+ qgrid: { logger: false } satisfies QgridProviderOptions,
279
+ },
280
+ experimental_telemetry: createQgridLogger({ serverUrl: "http://localhost:44900" }),
281
+ });
282
+ ```
283
+
284
+ External-provider request logs store model names as `provider/modelId`. If the provider reports
285
+ a different served model in the AI SDK response metadata, step and final log rows record that
286
+ observed model while retaining the requested model separately. This does not alter the AI SDK
287
+ runtime `response.modelId`. AI SDK adapter suffixes are normalized to the base provider, for
288
+ example `openai.responses` to `openai` and `anthropic.messages` to `anthropic`.
289
+
263
290
  ### Using alongside the qgrid provider
264
291
 
265
292
  The `qgrid()` provider has its own lifecycle, so the logger automatically suppresses itself for qgrid calls. Mixing the qgrid provider and other providers in the same code does not double-log.
@@ -279,6 +306,7 @@ type QgridSupportedModel =
279
306
  | "openai/gpt-5.3-codex"
280
307
  | "openai/gpt-5.3-codex-spark"
281
308
  // Anthropic
309
+ | "anthropic/claude-fable-5"
282
310
  | "anthropic/claude-haiku-4-5"
283
311
  | "anthropic/claude-sonnet-4"
284
312
  | "anthropic/claude-sonnet-4-5"
@@ -291,6 +319,7 @@ type QgridSupportedModel =
291
319
  | "anthropic/claude-opus-4-6"
292
320
  | "anthropic/claude-opus-4-7"
293
321
  | "anthropic/claude-opus-4-8"
322
+ | "anthropic/claude-opus-5"
294
323
  ```
295
324
 
296
325
  ### GPT-5.6 specifications
@@ -303,6 +332,14 @@ type QgridSupportedModel =
303
332
 
304
333
  All GPT-5.6 models support reasoning through `max`. The OpenAI native API spec is a 1.05M context window with 128K max output, but qgrid runs on the codex app-server subscription path, where all three models report a 372K context window (95% effective — about 353K of usable input) that cannot be raised by configuration. Prompts over 272K input tokens apply a 2x input and 1.5x output surcharge to the full request; cache writes cost 1.25x the uncached input rate.
305
334
 
335
+ `anthropic/claude-fable-5` has a 1M context window and 128K max output. Its standard prices per 1M tokens are $10 input, $1 cache read, $12.50 five-minute cache write, $20 one-hour cache write, and $50 output. qgrid preserves Claude's 5m/1h cache-creation breakdown and prices each TTL separately; only legacy responses without that breakdown fall back to the one-hour TTL automatically selected by Claude Code on subscription OAuth. Fable requires always-on adaptive thinking, so qgrid preserves adaptive thinking for this model.
336
+
337
+ `anthropic/claude-opus-5` has a default 1M context window and 128K max output. Its prices per 1M tokens are $5 input, $0.50 cache read, $6.25 five-minute cache write, $10 one-hour cache write, and $25 output. qgrid keeps Opus 5's default adaptive thinking behavior and uses `effort` to control reasoning depth. This also avoids the invalid `thinking: disabled` combination at `xhigh` or `max` effort.
338
+
339
+ Claude Code may automatically retry a Fable safety refusal on Opus 4.8. In that case, the AI SDK response's `response.modelId` and `providerMetadata.qgrid.model` identify Opus as the actual serving model. `providerMetadata.qgrid.requestedModel` remains Fable, and `providerMetadata.qgrid.modelFallbacks` contains the refusal fallback history. The metadata also exposes `costSource` and the 5m/1h cache-write token split.
340
+
341
+ `openai/gpt-5.3-codex-spark` remains a research preview without final published per-token rates. qgrid therefore reports its generic fallback estimate rather than presenting that estimate as official pricing.
342
+
306
343
  ## Configuration
307
344
 
308
345
  ```typescript
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { LanguageModelV3 } from "@ai-sdk/provider";
2
2
  import { TelemetrySettings } from "ai";
3
-
4
3
  //#region src/index.types.d.ts
5
4
  type QgridProviderConfig = {
6
5
  serverUrl?: string;
@@ -13,6 +12,11 @@ type QgridProviderConfig = {
13
12
  * providerOptions.qgrid 로 전달한다.
14
13
  */
15
14
  type QgridProviderOptions = {
15
+ /**
16
+ * qgrid request log 저장 여부. 기본값은 true.
17
+ * false여도 client tool 실행과 multi-step 연결은 계속 동작한다.
18
+ */
19
+ logger?: boolean;
16
20
  /**
17
21
  * 멀티턴 시 codex thread reuse를 위한 대화 식별자, 호출자가 자기 도메인 ID(예: 게임 세션 ID) 하나만 넘기면
18
22
  * provider가 같은 sessionKey 의 thread 좌표를 내부에서 회송해 prompt cache 를 적중시킨다
@@ -27,7 +31,10 @@ type QgridProviderOptions = {
27
31
  reasoningSummary?: "auto" | "concise" | "detailed" | "none";
28
32
  /** OpenAI/codex service tier. OpenAI/codex route에서만 적용된다. */
29
33
  serviceTier?: string;
30
- /** @todo 향후 qgrid 서버 fallback routing에 사용할 후보 모델 목록. */
34
+ /**
35
+ * @todo 향후 qgrid 서버 fallback routing에 사용할 후보 모델 목록.
36
+ * Claude Code가 처리하는 Fable 5 safety-refusal fallback과는 무관하다.
37
+ */
31
38
  fallbackModels?: string[];
32
39
  /**
33
40
  * codex 내장 image_generation tool 을 켠다. OpenAI/codex route + non-stream 전용.
@@ -44,13 +51,7 @@ type QgridProviderOptions = {
44
51
  size?: "1024x1024" | "1024x1536" | "1536x1024";
45
52
  };
46
53
  };
47
- /**
48
- * codex thread 좌표
49
- * 멀티턴 대화에서 같은 대화를 같은 codex thread 로 라우팅 → conversation_id 고정 → 캐시 적중
50
- * provider 내부 threadCoordStore 가 sessionKey 별로 관리
51
- */
52
-
53
- type QgridSupportedModel = "openai/gpt-5.6-sol" | "openai/gpt-5.6-terra" | "openai/gpt-5.6-luna" | "openai/gpt-5.5" | "openai/gpt-5.4" | "openai/gpt-5.2" | "openai/gpt-5.4-mini" | "openai/gpt-5.3-codex" | "openai/gpt-5.3-codex-spark" | "anthropic/claude-haiku-4-5" | "anthropic/claude-sonnet-4" | "anthropic/claude-sonnet-4-5" | "anthropic/claude-sonnet-4-6" | "anthropic/claude-sonnet-4-7" | "anthropic/claude-sonnet-5" | "anthropic/claude-opus-4" | "anthropic/claude-opus-4-1" | "anthropic/claude-opus-4-5" | "anthropic/claude-opus-4-6" | "anthropic/claude-opus-4-7" | "anthropic/claude-opus-4-8";
54
+ type QgridSupportedModel = "openai/gpt-5.6-sol" | "openai/gpt-5.6-terra" | "openai/gpt-5.6-luna" | "openai/gpt-5.5" | "openai/gpt-5.4" | "openai/gpt-5.2" | "openai/gpt-5.4-mini" | "openai/gpt-5.3-codex" | "openai/gpt-5.3-codex-spark" | "anthropic/claude-fable-5" | "anthropic/claude-haiku-4-5" | "anthropic/claude-sonnet-4" | "anthropic/claude-sonnet-4-5" | "anthropic/claude-sonnet-4-6" | "anthropic/claude-sonnet-4-7" | "anthropic/claude-sonnet-5" | "anthropic/claude-opus-4" | "anthropic/claude-opus-4-1" | "anthropic/claude-opus-4-5" | "anthropic/claude-opus-4-6" | "anthropic/claude-opus-4-7" | "anthropic/claude-opus-4-8" | "anthropic/claude-opus-5";
54
55
  type QgridLoggerConfig = {
55
56
  /** qgrid 서버 주소. default: process.env.QGRID_URL, 없으면 기본값은 "http://localhost:44900" */
56
57
  serverUrl?: string;
package/dist/index.js CHANGED
@@ -356,6 +356,12 @@ function serializeHistory(messages) {
356
356
  const DEFAULT_RUN_KEY = "__qgrid_default_run__";
357
357
  const DEFAULT_STALE_RUN_TIMEOUT_MS = 1800 * 1e3;
358
358
  const STALE_RUN_GRACE_MS = 5e3;
359
+ 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.";
360
+ function fullModelName(provider, modelId) {
361
+ if (!provider || !modelId) return void 0;
362
+ const prefix = `${provider.split(".", 1)[0] ?? provider}/`;
363
+ return modelId.startsWith(prefix) ? modelId : `${prefix}${modelId}`;
364
+ }
359
365
  function timedKeySet() {
360
366
  const keys = /* @__PURE__ */ new Set();
361
367
  const timers = /* @__PURE__ */ new Map();
@@ -385,8 +391,9 @@ function createQgridLogger(config = {}) {
385
391
  const projectName = config.projectName ?? process.env.QGRID_PROJECT_NAME;
386
392
  const onLogError = config.onLogError ?? ((e) => console.warn(`[qgrid-logger] ${e.message}`));
387
393
  const runs = /* @__PURE__ */ new Map();
394
+ const starting = /* @__PURE__ */ new Map();
388
395
  const keyTtl = typeof config.staleRunTimeoutMs === "number" && config.staleRunTimeoutMs > 0 ? config.staleRunTimeoutMs : DEFAULT_STALE_RUN_TIMEOUT_MS;
389
- const suppressedQgrid = timedKeySet();
396
+ const suppressedRuns = timedKeySet();
390
397
  const quarantined = timedKeySet();
391
398
  const finalizeRun = async (runKey, result) => {
392
399
  const run = runs.get(runKey);
@@ -403,6 +410,8 @@ function createQgridLogger(config = {}) {
403
410
  toolCallId: pending.toolCallId,
404
411
  toolName: pending.toolName,
405
412
  toolArgs: pending.toolArgs,
413
+ modelName: pending.modelName,
414
+ requestedModelName: pending.requestedModelName,
406
415
  toolDurationMs: run.toolDurations.get(pending.toolCallId)
407
416
  }).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e)))));
408
417
  run.pendingToolCalls = [];
@@ -412,6 +421,8 @@ function createQgridLogger(config = {}) {
412
421
  status: result.status,
413
422
  response: result.response,
414
423
  tokenName: config.tokenName ?? "external",
424
+ modelName: result.modelName,
425
+ requestedModelName: result.requestedModelName ?? run.requestedModelName,
415
426
  totalInputTokens: result.totalUsage?.inputTokens ?? 0,
416
427
  totalOutputTokens: result.totalUsage?.outputTokens ?? 0,
417
428
  totalCacheReadTokens: result.totalUsage?.inputTokenDetails?.cacheReadTokens ?? 0,
@@ -433,34 +444,63 @@ function createQgridLogger(config = {}) {
433
444
  async onStart(event) {
434
445
  if (!event.metadata?.qgridRunId && !event.functionId && event.metadata) event.metadata.qgridRunId = `auto-${++autoRunIdCounter}`;
435
446
  const runKey = resolveRunKey(event);
447
+ if (event.model.provider === "qgrid") return;
436
448
  if (quarantined.has(runKey)) {
437
449
  onLogError(/* @__PURE__ */ new Error("createQgridLogger: telemetry key is quarantined after overlap"));
438
450
  return;
439
451
  }
440
- if (event.model.provider === "qgrid") {
441
- suppressedQgrid.add(runKey, keyTtl);
442
- return;
443
- }
444
- if (runs.has(runKey)) {
445
- const msg = "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.";
446
- await finalizeRun(runKey, {
452
+ if (runs.has(runKey) || starting.has(runKey) || suppressedRuns.has(runKey)) {
453
+ const reservation = starting.get(runKey);
454
+ if (reservation) {
455
+ reservation.errorMessage = OVERLAPPING_RUN_ERROR;
456
+ starting.delete(runKey);
457
+ }
458
+ suppressedRuns.remove(runKey);
459
+ quarantined.add(runKey, keyTtl);
460
+ const finalizing = finalizeRun(runKey, {
447
461
  status: "error",
448
- errorMessage: msg
462
+ errorMessage: OVERLAPPING_RUN_ERROR
449
463
  });
450
- quarantined.add(runKey, keyTtl);
451
- onLogError(new Error(msg));
464
+ onLogError(/* @__PURE__ */ new Error(OVERLAPPING_RUN_ERROR));
465
+ await finalizing;
466
+ return;
467
+ }
468
+ if (event.providerOptions?.qgrid?.logger === false) {
469
+ suppressedRuns.add(runKey, keyTtl);
452
470
  return;
453
471
  }
472
+ const requestedModelName = fullModelName(event.model.provider, event.model.modelId) ?? event.model.modelId;
473
+ const reservation = {
474
+ requestedModelName,
475
+ startTime: Date.now()
476
+ };
477
+ starting.set(runKey, reservation);
454
478
  try {
455
479
  const messages = event.messages ?? (Array.isArray(event.prompt) ? event.prompt : void 0);
456
480
  const history = serializeHistory(messages);
457
481
  const result = await createRun(serverUrl, {
458
482
  userPrompt: extractUserPrompt(event.prompt, messages),
459
483
  systemPrompt: extractSystemPrompt(event.system),
460
- modelName: event.model.modelId,
484
+ modelName: requestedModelName,
461
485
  projectName,
462
486
  history
463
487
  });
488
+ if (reservation.errorMessage || quarantined.has(runKey) || starting.get(runKey) !== reservation) {
489
+ if (starting.get(runKey) === reservation) starting.delete(runKey);
490
+ await finishRun(serverUrl, {
491
+ requestLogId: result.requestLogId,
492
+ status: "error",
493
+ tokenName: config.tokenName ?? "external",
494
+ requestedModelName: reservation.requestedModelName,
495
+ totalInputTokens: 0,
496
+ totalOutputTokens: 0,
497
+ totalCacheReadTokens: 0,
498
+ totalCacheCreationTokens: 0,
499
+ totalDurationMs: Date.now() - reservation.startTime,
500
+ errorMessage: reservation.errorMessage ?? OVERLAPPING_RUN_ERROR
501
+ }).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e))));
502
+ return;
503
+ }
464
504
  let watchdogTimeout = DEFAULT_STALE_RUN_TIMEOUT_MS;
465
505
  if (typeof config.staleRunTimeoutMs === "number") watchdogTimeout = config.staleRunTimeoutMs;
466
506
  else if (typeof event.timeout === "number" && event.timeout > 0) watchdogTimeout = event.timeout + STALE_RUN_GRACE_MS;
@@ -493,8 +533,10 @@ function createQgridLogger(config = {}) {
493
533
  cleanupAbortListener = () => signal.removeEventListener("abort", onAbort);
494
534
  }
495
535
  }
536
+ starting.delete(runKey);
496
537
  runs.set(runKey, {
497
538
  requestLogId: result.requestLogId,
539
+ requestedModelName,
498
540
  pendingSteps: [],
499
541
  pendingToolCalls: [],
500
542
  startTime: Date.now(),
@@ -504,22 +546,27 @@ function createQgridLogger(config = {}) {
504
546
  finishing: false
505
547
  });
506
548
  } catch (e) {
549
+ if (starting.get(runKey) === reservation) starting.delete(runKey);
507
550
  onLogError(e instanceof Error ? e : new Error(String(e)));
508
551
  }
509
552
  },
510
553
  onToolCallFinish(event) {
554
+ if (event.model?.provider === "qgrid") return;
511
555
  const runKey = resolveRunKey(event);
512
- if (suppressedQgrid.has(runKey) || quarantined.has(runKey)) return;
556
+ if (suppressedRuns.has(runKey) || quarantined.has(runKey)) return;
513
557
  const run = runs.get(runKey);
514
558
  if (!run) return;
515
559
  run.toolDurations.set(event.toolCall.toolCallId, Math.round(event.durationMs));
516
560
  },
517
561
  onStepFinish(event) {
562
+ if (event.model?.provider === "qgrid") return;
518
563
  const runKey = resolveRunKey(event);
519
- if (suppressedQgrid.has(runKey) || quarantined.has(runKey)) return;
564
+ if (suppressedRuns.has(runKey) || quarantined.has(runKey)) return;
520
565
  const run = runs.get(runKey);
521
566
  if (!run) return;
522
567
  const { content, usage, reasoningText, finishReason, stepNumber } = event;
568
+ const requestedModelName = fullModelName(event.model?.provider, event.model?.modelId) ?? run.requestedModelName;
569
+ const modelName = fullModelName(event.model?.provider, event.response?.modelId);
523
570
  const remainingPending = [];
524
571
  for (const pending of run.pendingToolCalls) {
525
572
  const tr = content.find((p) => p.type === "tool-result" && p.toolCallId === pending.toolCallId);
@@ -533,6 +580,8 @@ function createQgridLogger(config = {}) {
533
580
  toolCallId: pending.toolCallId,
534
581
  toolName: pending.toolName,
535
582
  toolArgs: pending.toolArgs,
583
+ modelName: pending.modelName,
584
+ requestedModelName: pending.requestedModelName,
536
585
  toolResult: tr && "output" in tr ? safeStringify(tr.output) : void 0,
537
586
  toolDurationMs: run.toolDurations.get(pending.toolCallId),
538
587
  error: te && "error" in te ? safeStringify(te.error) : void 0
@@ -545,6 +594,8 @@ function createQgridLogger(config = {}) {
545
594
  requestLogId: run.requestLogId,
546
595
  stepIndex: stepNumber,
547
596
  type: "generate",
597
+ modelName,
598
+ requestedModelName,
548
599
  inputTokens: usage.inputTokens ?? 0,
549
600
  outputTokens: usage.outputTokens ?? 0,
550
601
  cacheReadTokens: usage.inputTokenDetails?.cacheReadTokens ?? 0,
@@ -566,6 +617,8 @@ function createQgridLogger(config = {}) {
566
617
  toolCallId: tc.toolCallId,
567
618
  toolName: tc.toolName,
568
619
  toolArgs: safeStringify(tc.input),
620
+ modelName,
621
+ requestedModelName,
569
622
  toolResult: tr && "output" in tr ? safeStringify(tr.output) : void 0,
570
623
  toolDurationMs: run.toolDurations.get(tc.toolCallId),
571
624
  error: te && "error" in te ? safeStringify(te.error) : void 0
@@ -576,23 +629,31 @@ function createQgridLogger(config = {}) {
576
629
  toolCallIndex: i,
577
630
  toolCallId: tc.toolCallId,
578
631
  toolName: tc.toolName,
579
- toolArgs: safeStringify(tc.input)
632
+ toolArgs: safeStringify(tc.input),
633
+ modelName,
634
+ requestedModelName
580
635
  });
581
636
  }
582
637
  },
583
638
  async onFinish(event) {
639
+ if (event.model?.provider === "qgrid") return;
584
640
  const runKey = resolveRunKey(event);
585
- if (suppressedQgrid.has(runKey)) {
586
- suppressedQgrid.remove(runKey);
641
+ if (suppressedRuns.has(runKey)) {
642
+ suppressedRuns.remove(runKey);
587
643
  return;
588
644
  }
589
645
  if (quarantined.has(runKey)) return;
590
- if (!runs.get(runKey)) return;
646
+ const run = runs.get(runKey);
647
+ if (!run) return;
591
648
  const status = event.finishReason === "error" ? "error" : "succeeded";
649
+ const requestedModelName = fullModelName(event.model?.provider, event.model?.modelId) ?? run.requestedModelName;
650
+ const modelName = fullModelName(event.model?.provider, event.response?.modelId);
592
651
  await finalizeRun(runKey, {
593
652
  status,
594
653
  response: event.text,
595
654
  totalUsage: event.totalUsage,
655
+ modelName,
656
+ requestedModelName,
596
657
  ...status === "error" && "error" in event ? { errorMessage: getErrorMessage(event.error) } : {}
597
658
  });
598
659
  }
@@ -632,6 +693,19 @@ function setThreadCoord(sessionKey, coord) {
632
693
  expiresAt: Date.now() + THREAD_COORD_TTL_MS
633
694
  });
634
695
  }
696
+ function qgridProviderMetadata(data) {
697
+ return { qgrid: {
698
+ model: data.model,
699
+ requestedModel: data.requestedModel ?? data.model,
700
+ modelFallbacks: data.modelFallbacks ?? [],
701
+ tokenName: data.tokenName ?? null,
702
+ durationMs: data.durationMs,
703
+ costUsd: data.costUsd,
704
+ costSource: data.costSource,
705
+ cacheCreation5mInputTokens: data.usage.cache_creation_5m_input_tokens ?? null,
706
+ cacheCreation1hInputTokens: data.usage.cache_creation_1h_input_tokens ?? null
707
+ } };
708
+ }
635
709
  function reusableSessionKey(modelId, sessionKey) {
636
710
  return modelId.startsWith("anthropic/") ? void 0 : sessionKey;
637
711
  }
@@ -662,8 +736,8 @@ function qgrid(modelId, config) {
662
736
  const clientRuns = /* @__PURE__ */ new Map();
663
737
  let anonymousClientRunId = 0;
664
738
  function clientRunKey(runContext) {
665
- if (runContext.requestLogId !== void 0) return `request:${runContext.requestLogId}`;
666
- if (runContext.threadCoord) {
739
+ if (runContext?.requestLogId !== void 0) return `request:${runContext.requestLogId}`;
740
+ if (runContext?.threadCoord) {
667
741
  const { workerId, epoch, threadId } = runContext.threadCoord;
668
742
  return `thread:${workerId}:${epoch}:${threadId}`;
669
743
  }
@@ -708,6 +782,7 @@ function qgrid(modelId, config) {
708
782
  const verbosity = qgridOptions.verbosity;
709
783
  const reasoningSummary = qgridOptions.reasoningSummary;
710
784
  const serviceTier = qgridOptions.serviceTier;
785
+ const logger = qgridOptions.logger;
711
786
  const imageGenerationOptions = qgridOptions.imageGenerationOptions;
712
787
  if (imageGeneration) assertImageInputFitsJsonTransport(imageUrls);
713
788
  const sessionKey = qgridOptions.sessionKey;
@@ -718,16 +793,13 @@ function qgrid(modelId, config) {
718
793
  const jsonSchema = !hasTools && rawSchema && schemaType === "object" ? JSON.stringify(rawSchema) : void 0;
719
794
  let runContext;
720
795
  let toolResultsPayload;
721
- let logMode;
722
796
  const matchedClientRun = matchClientRun(options.prompt);
723
797
  if (matchedClientRun) {
724
798
  runContext = matchedClientRun.runContext;
725
799
  toolResultsPayload = matchedClientRun.toolResultsPayload;
726
- logMode = "run";
727
800
  }
728
801
  const storedCoord = reuseSessionKey ? getThreadCoord(reuseSessionKey) : void 0;
729
- if (!runContext && storedCoord) runContext = { threadCoord: storedCoord };
730
- if (!logMode && hasTools) logMode = "run";
802
+ if (!matchedClientRun && storedCoord) runContext = { threadCoord: storedCoord };
731
803
  const data = await fetch(`${serverUrl}/api/qgrid/query`, {
732
804
  method: "POST",
733
805
  headers: { "Content-Type": "application/json" },
@@ -744,7 +816,7 @@ function qgrid(modelId, config) {
744
816
  ...jsonSchema ? { jsonSchema } : {},
745
817
  ...history.length > 0 ? { history: JSON.stringify(history) } : {},
746
818
  ...projectName ? { projectName } : {},
747
- ...logMode ? { logMode } : {},
819
+ ...logger === false ? { logger: false } : {},
748
820
  ...runContext ? { runContext } : {},
749
821
  ...toolResultsPayload ? { toolResults: toolResultsPayload } : {},
750
822
  ...imageGeneration ? { imageGeneration } : {},
@@ -787,21 +859,16 @@ function qgrid(modelId, config) {
787
859
  type: "text",
788
860
  text: data.text
789
861
  });
790
- if (imageGeneration && !content.some((c) => c.type === "file")) throw new Error("qgrid: imageGeneration was requested but the response contained no image — the server may be an older version that does not support image generation.");
862
+ if (imageGeneration && finishReason.unified !== "tool-calls" && !content.some((c) => c.type === "file")) throw new Error("qgrid: imageGeneration was requested but the response contained no image — the server may be an older version that does not support image generation.");
791
863
  if (matchedClientRun) clientRuns.delete(matchedClientRun.key);
792
- if (data.runContext && finishReason.unified === "tool-calls") rememberClientRun(data.runContext, content.filter((c) => c.type === "tool-call").map((c) => c.toolCallId));
864
+ if (finishReason.unified === "tool-calls") rememberClientRun(data.runContext, content.filter((c) => c.type === "tool-call").map((c) => c.toolCallId));
793
865
  if (reuseSessionKey && data.runContext?.threadCoord) setThreadCoord(reuseSessionKey, data.runContext.threadCoord);
794
866
  return {
795
867
  content,
796
868
  finishReason,
797
869
  usage: toAiSdkUsage(data.usage),
798
870
  warnings: [],
799
- providerMetadata: { qgrid: {
800
- model: data.model,
801
- tokenName: data.tokenName ?? null,
802
- durationMs: data.durationMs,
803
- costUsd: data.costUsd
804
- } },
871
+ providerMetadata: qgridProviderMetadata(data),
805
872
  response: { modelId: data.model }
806
873
  };
807
874
  },
@@ -813,6 +880,7 @@ function qgrid(modelId, config) {
813
880
  const verbosity = qgridOptions.verbosity;
814
881
  const reasoningSummary = qgridOptions.reasoningSummary;
815
882
  const serviceTier = qgridOptions.serviceTier;
883
+ const logger = qgridOptions.logger;
816
884
  if (qgridOptions.imageGeneration) throw new Error("qgrid: imageGeneration is not supported with streamText — use generateText instead.");
817
885
  const { prompt, system, history, droppedImageCount } = extractPromptAndHistory(options.prompt, { includeImages: false });
818
886
  warnDroppedImages(droppedImageCount);
@@ -822,16 +890,13 @@ function qgrid(modelId, config) {
822
890
  const jsonSchema = !hasTools && rawSchema && rawSchema.type === "object" ? JSON.stringify(rawSchema) : void 0;
823
891
  let runContext;
824
892
  let toolResultsPayload;
825
- let logMode;
826
893
  const matchedClientRun = matchClientRun(options.prompt);
827
894
  if (matchedClientRun) {
828
895
  runContext = matchedClientRun.runContext;
829
896
  toolResultsPayload = matchedClientRun.toolResultsPayload;
830
- logMode = "run";
831
897
  }
832
898
  const storedCoord = reuseSessionKey ? getThreadCoord(reuseSessionKey) : void 0;
833
- if (!runContext && storedCoord) runContext = { threadCoord: storedCoord };
834
- if (!logMode && hasTools) logMode = "run";
899
+ if (!matchedClientRun && storedCoord) runContext = { threadCoord: storedCoord };
835
900
  const prepRes = await fetch(`${serverUrl}/api/qgrid/prepareStream`, {
836
901
  method: "POST",
837
902
  headers: { "Content-Type": "application/json" },
@@ -847,7 +912,7 @@ function qgrid(modelId, config) {
847
912
  ...jsonSchema ? { jsonSchema } : {},
848
913
  ...history.length > 0 ? { history: JSON.stringify(history) } : {},
849
914
  ...projectName ? { projectName } : {},
850
- ...logMode ? { logMode } : {},
915
+ ...logger === false ? { logger: false } : {},
851
916
  ...runContext ? { runContext } : {},
852
917
  ...toolResultsPayload ? { toolResults: toolResultsPayload } : {}
853
918
  } }),
@@ -895,7 +960,7 @@ function qgrid(modelId, config) {
895
960
  }
896
961
  const done = event.data;
897
962
  if (matchedClientRun) clientRuns.delete(matchedClientRun.key);
898
- if (done.runContext && done.finishReason === "tool-calls") rememberClientRun(done.runContext, (done.content ?? []).filter((c) => c.type === "tool-call").map((c) => c.toolCallId));
963
+ if (done.finishReason === "tool-calls") rememberClientRun(done.runContext, (done.content ?? []).filter((c) => c.type === "tool-call").map((c) => c.toolCallId));
899
964
  if (reuseSessionKey && done.runContext?.threadCoord) setThreadCoord(reuseSessionKey, done.runContext.threadCoord);
900
965
  if (done.content) {
901
966
  for (const item of done.content) if (item.type === "text" && !deltaTextEmitted) {
@@ -936,6 +1001,10 @@ function qgrid(modelId, config) {
936
1001
  });
937
1002
  }
938
1003
  }
1004
+ controller.enqueue({
1005
+ type: "response-metadata",
1006
+ modelId: done.model
1007
+ });
939
1008
  controller.enqueue({
940
1009
  type: "finish",
941
1010
  finishReason: done.finishReason === "tool-calls" ? {
@@ -945,7 +1014,8 @@ function qgrid(modelId, config) {
945
1014
  unified: "stop",
946
1015
  raw: "stop"
947
1016
  },
948
- usage: toAiSdkUsage(done.usage)
1017
+ usage: toAiSdkUsage(done.usage),
1018
+ providerMetadata: qgridProviderMetadata(done)
949
1019
  });
950
1020
  streamCompleted = true;
951
1021
  controller.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartanova/qgrid-ai-sdk",
3
- "version": "2.4.7",
3
+ "version": "2.4.9",
4
4
  "description": "AI SDK LanguageModelV3 provider for qgrid",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,16 +22,16 @@
22
22
  "devDependencies": {
23
23
  "@ai-sdk/provider": "^3.0.0",
24
24
  "ai": "^6.0.190",
25
- "tsdown": "^0.12.9",
25
+ "tsdown": "^0.22.3",
26
26
  "typescript": "^5.9.3",
27
- "vitest": "^4.0.10"
27
+ "vitest": "^4.1.10"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "@ai-sdk/provider": "^3.0.0",
31
31
  "ai": "^6.0.0"
32
32
  },
33
33
  "scripts": {
34
- "build": "tsdown && mv dist/index-*.d.ts dist/index.d.ts",
34
+ "build": "tsdown",
35
35
  "test": "vitest run",
36
36
  "e2e": "tsx e2e/e2e.ts",
37
37
  "e2e:logger": "tsx e2e/e2e-logger.ts"