@cartanova/qgrid-ai-sdk 2.4.8 → 2.5.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 CHANGED
@@ -162,6 +162,7 @@ const { text } = await generateText({
162
162
 
163
163
  | 옵션 | 값 | 적용 범위 | 설명 |
164
164
  |---|---|---|---|
165
+ | `logger` | `boolean` | 공통 | qgrid request log 저장 여부. 기본값은 `true`. `false`로 설정해도 client tool 실행과 multi-step 연결은 계속 동작 |
165
166
  | `sessionKey` | `string` | OpenAI 전용 | 멀티턴 대화 식별자. 같은 key는 같은 codex thread로 라우팅되어 prompt cache 적중 ([아래](#멀티턴-prompt-cache-sessionkey) 참조) |
166
167
  | `effort` | `"none"` \| `"minimal"` \| `"low"` \| `"medium"` \| `"high"` \| `"xhigh"` \| `"max"` | 공통 (지원 값은 모델별 상이, 예: `"max"`는 GPT-5.6+) | reasoning 모델의 추론 깊이. 기본값은 config의 `defaultEffort` (`"low"`) |
167
168
  | `verbosity` | `"low"` \| `"medium"` \| `"high"` | OpenAI 전용 | 응답 텍스트의 상세도 |
@@ -263,6 +264,29 @@ createQgridLogger({
263
264
 
264
265
  모든 설정은 optional (serverUrl 제외). 기본값이 있으므로 `serverUrl`만 넣으면 동작합니다.
265
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
+
266
290
  ### qgrid provider와 함께 사용
267
291
 
268
292
  `qgrid()` provider는 자체 lifecycle이 있으므로 logger가 자동으로 suppress됩니다. 같은 코드에서 qgrid provider와 다른 provider를 섞어 써도 이중 기록되지 않습니다.
@@ -295,6 +319,7 @@ type QgridSupportedModel =
295
319
  | "anthropic/claude-opus-4-6"
296
320
  | "anthropic/claude-opus-4-7"
297
321
  | "anthropic/claude-opus-4-8"
322
+ | "anthropic/claude-opus-5"
298
323
  ```
299
324
 
300
325
  ### GPT-5.6 사양
@@ -307,7 +332,9 @@ type QgridSupportedModel =
307
332
 
308
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입니다.
309
334
 
310
- `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을 보존하고, 기존 Anthropic 모델은 계속 thinking을 비활성화합니다.
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` 조합도 만들지 않습니다.
311
338
 
312
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 토큰 분해도 확인할 수 있습니다.
313
340
 
package/README.md CHANGED
@@ -162,6 +162,7 @@ const { text } = await generateText({
162
162
 
163
163
  | Option | Values | Applies to | Description |
164
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 |
165
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)) |
166
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"`) |
167
168
  | `verbosity` | `"low"` \| `"medium"` \| `"high"` | OpenAI only | Response text verbosity |
@@ -263,6 +264,29 @@ createQgridLogger({
263
264
 
264
265
  Everything except `serverUrl` is optional. With defaults in place, passing only `serverUrl` works.
265
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
+
266
290
  ### Using alongside the qgrid provider
267
291
 
268
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.
@@ -295,6 +319,7 @@ type QgridSupportedModel =
295
319
  | "anthropic/claude-opus-4-6"
296
320
  | "anthropic/claude-opus-4-7"
297
321
  | "anthropic/claude-opus-4-8"
322
+ | "anthropic/claude-opus-5"
298
323
  ```
299
324
 
300
325
  ### GPT-5.6 specifications
@@ -307,7 +332,9 @@ type QgridSupportedModel =
307
332
 
308
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.
309
334
 
310
- `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 while continuing to disable thinking for existing Anthropic models.
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.
311
338
 
312
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.
313
340
 
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 를 적중시킨다
@@ -47,13 +51,7 @@ type QgridProviderOptions = {
47
51
  size?: "1024x1024" | "1024x1536" | "1536x1024";
48
52
  };
49
53
  };
50
- /**
51
- * codex thread 좌표
52
- * 멀티턴 대화에서 같은 대화를 같은 codex thread 로 라우팅 → conversation_id 고정 → 캐시 적중
53
- * provider 내부 threadCoordStore 가 sessionKey 별로 관리
54
- */
55
-
56
- 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";
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";
57
55
  type QgridLoggerConfig = {
58
56
  /** qgrid 서버 주소. default: process.env.QGRID_URL, 없으면 기본값은 "http://localhost:44900" */
59
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;
452
466
  return;
453
467
  }
468
+ if (event.providerOptions?.qgrid?.logger === false) {
469
+ suppressedRuns.add(runKey, keyTtl);
470
+ return;
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
  }
@@ -675,8 +736,8 @@ function qgrid(modelId, config) {
675
736
  const clientRuns = /* @__PURE__ */ new Map();
676
737
  let anonymousClientRunId = 0;
677
738
  function clientRunKey(runContext) {
678
- if (runContext.requestLogId !== void 0) return `request:${runContext.requestLogId}`;
679
- if (runContext.threadCoord) {
739
+ if (runContext?.requestLogId !== void 0) return `request:${runContext.requestLogId}`;
740
+ if (runContext?.threadCoord) {
680
741
  const { workerId, epoch, threadId } = runContext.threadCoord;
681
742
  return `thread:${workerId}:${epoch}:${threadId}`;
682
743
  }
@@ -721,6 +782,7 @@ function qgrid(modelId, config) {
721
782
  const verbosity = qgridOptions.verbosity;
722
783
  const reasoningSummary = qgridOptions.reasoningSummary;
723
784
  const serviceTier = qgridOptions.serviceTier;
785
+ const logger = qgridOptions.logger;
724
786
  const imageGenerationOptions = qgridOptions.imageGenerationOptions;
725
787
  if (imageGeneration) assertImageInputFitsJsonTransport(imageUrls);
726
788
  const sessionKey = qgridOptions.sessionKey;
@@ -731,16 +793,13 @@ function qgrid(modelId, config) {
731
793
  const jsonSchema = !hasTools && rawSchema && schemaType === "object" ? JSON.stringify(rawSchema) : void 0;
732
794
  let runContext;
733
795
  let toolResultsPayload;
734
- let logMode;
735
796
  const matchedClientRun = matchClientRun(options.prompt);
736
797
  if (matchedClientRun) {
737
798
  runContext = matchedClientRun.runContext;
738
799
  toolResultsPayload = matchedClientRun.toolResultsPayload;
739
- logMode = "run";
740
800
  }
741
801
  const storedCoord = reuseSessionKey ? getThreadCoord(reuseSessionKey) : void 0;
742
- if (!runContext && storedCoord) runContext = { threadCoord: storedCoord };
743
- if (!logMode && hasTools) logMode = "run";
802
+ if (!matchedClientRun && storedCoord) runContext = { threadCoord: storedCoord };
744
803
  const data = await fetch(`${serverUrl}/api/qgrid/query`, {
745
804
  method: "POST",
746
805
  headers: { "Content-Type": "application/json" },
@@ -757,7 +816,7 @@ function qgrid(modelId, config) {
757
816
  ...jsonSchema ? { jsonSchema } : {},
758
817
  ...history.length > 0 ? { history: JSON.stringify(history) } : {},
759
818
  ...projectName ? { projectName } : {},
760
- ...logMode ? { logMode } : {},
819
+ ...logger === false ? { logger: false } : {},
761
820
  ...runContext ? { runContext } : {},
762
821
  ...toolResultsPayload ? { toolResults: toolResultsPayload } : {},
763
822
  ...imageGeneration ? { imageGeneration } : {},
@@ -800,9 +859,9 @@ function qgrid(modelId, config) {
800
859
  type: "text",
801
860
  text: data.text
802
861
  });
803
- 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.");
804
863
  if (matchedClientRun) clientRuns.delete(matchedClientRun.key);
805
- 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));
806
865
  if (reuseSessionKey && data.runContext?.threadCoord) setThreadCoord(reuseSessionKey, data.runContext.threadCoord);
807
866
  return {
808
867
  content,
@@ -821,6 +880,7 @@ function qgrid(modelId, config) {
821
880
  const verbosity = qgridOptions.verbosity;
822
881
  const reasoningSummary = qgridOptions.reasoningSummary;
823
882
  const serviceTier = qgridOptions.serviceTier;
883
+ const logger = qgridOptions.logger;
824
884
  if (qgridOptions.imageGeneration) throw new Error("qgrid: imageGeneration is not supported with streamText — use generateText instead.");
825
885
  const { prompt, system, history, droppedImageCount } = extractPromptAndHistory(options.prompt, { includeImages: false });
826
886
  warnDroppedImages(droppedImageCount);
@@ -830,16 +890,13 @@ function qgrid(modelId, config) {
830
890
  const jsonSchema = !hasTools && rawSchema && rawSchema.type === "object" ? JSON.stringify(rawSchema) : void 0;
831
891
  let runContext;
832
892
  let toolResultsPayload;
833
- let logMode;
834
893
  const matchedClientRun = matchClientRun(options.prompt);
835
894
  if (matchedClientRun) {
836
895
  runContext = matchedClientRun.runContext;
837
896
  toolResultsPayload = matchedClientRun.toolResultsPayload;
838
- logMode = "run";
839
897
  }
840
898
  const storedCoord = reuseSessionKey ? getThreadCoord(reuseSessionKey) : void 0;
841
- if (!runContext && storedCoord) runContext = { threadCoord: storedCoord };
842
- if (!logMode && hasTools) logMode = "run";
899
+ if (!matchedClientRun && storedCoord) runContext = { threadCoord: storedCoord };
843
900
  const prepRes = await fetch(`${serverUrl}/api/qgrid/prepareStream`, {
844
901
  method: "POST",
845
902
  headers: { "Content-Type": "application/json" },
@@ -855,7 +912,7 @@ function qgrid(modelId, config) {
855
912
  ...jsonSchema ? { jsonSchema } : {},
856
913
  ...history.length > 0 ? { history: JSON.stringify(history) } : {},
857
914
  ...projectName ? { projectName } : {},
858
- ...logMode ? { logMode } : {},
915
+ ...logger === false ? { logger: false } : {},
859
916
  ...runContext ? { runContext } : {},
860
917
  ...toolResultsPayload ? { toolResults: toolResultsPayload } : {}
861
918
  } }),
@@ -903,7 +960,7 @@ function qgrid(modelId, config) {
903
960
  }
904
961
  const done = event.data;
905
962
  if (matchedClientRun) clientRuns.delete(matchedClientRun.key);
906
- 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));
907
964
  if (reuseSessionKey && done.runContext?.threadCoord) setThreadCoord(reuseSessionKey, done.runContext.threadCoord);
908
965
  if (done.content) {
909
966
  for (const item of done.content) if (item.type === "text" && !deltaTextEmitted) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartanova/qgrid-ai-sdk",
3
- "version": "2.4.8",
3
+ "version": "2.5.0",
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"