@cartanova/qgrid-ai-sdk 2.9.10 → 2.10.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
@@ -212,7 +212,7 @@ const { text } = await generateText({
212
212
  | `timeoutMs` | 양의 정수, 최대 `1_800_000` | Anthropic 전용 | 서버의 Claude Code 프로세스 제한시간(ms). SDK의 non-stream HTTP 제한은 이 값보다 60초 길게 설정. 기본값은 240초 |
213
213
  | `imageGeneration` | `boolean` | OpenAI 전용, non-stream | codex 내장 `image_generation` tool 활성화 ([아래](#image-generation) 참조) |
214
214
  | `imageGenerationOptions` | `{ quality?, size?, background? }` | OpenAI 전용 | 이미지 생성 옵션. `background: "transparent"`는 전용 경로 사용. 실제 크기·품질은 응답 metadata 확인 ([아래](#image-generation)). |
215
- | `fallbackModels` | `string[]` | 예약 | 향후 qgrid 서버 fallback routing용 예약 필드. 현재 동작하지 않으며 Claude Code의 Fable refusal fallback과 무관 |
215
+ | `fallbackModels` | `string[]` | deprecated | 호환용으로만 남긴 무시되는 필드. 사용하면 unsupported-option 경고를 반환하므로 호출부에서 제거하세요. Claude Code의 Fable refusal fallback과 무관 |
216
216
 
217
217
  ```typescript
218
218
  await generateText({
@@ -300,6 +300,8 @@ const result = await generateText({
300
300
 
301
301
  ## Telemetry Logger
302
302
 
303
+ 서버와 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가 추가되지 않습니다.
304
+
303
305
  qgrid provider가 아닌 모델(google, openai 직접 호출)에서도 같은 request log 대시보드를 사용하려면 `createQgridLogger`를 `experimental_telemetry`에 넣으면 됩니다.
304
306
 
305
307
  ```typescript
@@ -398,6 +400,7 @@ type QgridSupportedModel =
398
400
  | "anthropic/claude-opus-5"
399
401
  | "anthropic/claude-opus-5-5"
400
402
  | "anthropic/claude-sonnet-5-5"
403
+ | "anthropic/claude-haiku-5-5"
401
404
  ```
402
405
 
403
406
  `openai/gpt-5.4`, `openai/gpt-5.4-mini`, `openai/gpt-5.2`, `openai/gpt-5.3-codex`는 하위 호환을 위해 타입에 남아 있지만, qgrid가 사용하는 ChatGPT 구독 Codex 경로에서는 더 이상 제공되지 않습니다. `gpt-5.4`와 `gpt-5.4-mini`는 2026-08-31에 retire되었고(대체: `openai/gpt-5.6-terra`, `openai/gpt-5.6-luna`), `gpt-5.2`와 `gpt-5.3-codex`는 그보다 먼저 해당 경로에서 제거되었습니다. 이 id로 요청하면 백엔드에서 실패합니다.
@@ -433,7 +436,9 @@ Qgrid 비용 추정에는 [표준 API 단가](https://developers.openai.com/api/
433
436
 
434
437
  `anthropic/claude-opus-5-5`(2026-09-22 출시)는 1M context와 128K 최대 출력을 지원합니다. 1M tokens당 단가는 input $4, cache read $0.20(input의 0.05x), 5분 cache write $5, 1시간 cache write $8, output $20입니다. adaptive thinking이 항상 켜져 있고 끌 수 없으므로 qgrid는 Fable과 같이 이를 보존하고 `effort`로 추론 깊이를 조절합니다. Opus 5.5의 API 수준 파괴적 변경(forced `tool_choice` 거부, 모델에 귀속된 thinking 블록)은 Fable 5.1과 같은 이유로 qgrid에 영향을 주지 않습니다. Opus 5 단가는 변동이 없습니다.
435
438
 
436
- `anthropic/claude-sonnet-5-5`(2026-09-29 출시)는 1M context와 128K 최대 출력을 지원하며 Sonnet 5와 같은 단가입니다. 1M tokens당 input $2, cache read $0.20, 5분 cache write $2.50, 1시간 cache write $4, output $10입니다. thinking 비활성화를 거부하므로 qgrid는 Opus 5.5와 같이 adaptive thinking을 유지하고 `effort`로 추론 깊이를 조절합니다.
439
+ `anthropic/claude-sonnet-5-5`(2026-09-28 출시)는 1M context와 128K 최대 출력을 지원하며 Sonnet 5와 같은 기본 단가입니다. 1M tokens당 input $2, 5분 cache write $2.50, 1시간 cache write $4, output $10이고, cache read는 2026-10-07에 $0.20에서 절반으로 인하되어 Opus 5.5와 같은 0.05x인 $0.10입니다. qgrid는 요청 시점 단가로 비용을 기록하므로 그 이전 로그는 $0.20 추정치를 유지합니다. thinking 비활성화를 거부하므로 qgrid는 Opus 5.5와 같이 adaptive thinking을 유지하고 `effort`로 추론 깊이를 조절합니다.
440
+
441
+ `anthropic/claude-haiku-5-5`(2026-10-07 출시)는 1M context와 128K 최대 출력을 지원하며, 현행 Claude 모델 중 유일하게 프롬프트 길이별 단가를 씁니다. 100K 토큰 이하 프롬프트는 1M tokens당 input $0.10, cache read $0.01, 5분 cache write $0.125, 1시간 cache write $0.20, output $0.50이고, 프롬프트가 100K 토큰을 넘으면 모든 단가가 5배($0.50 / $0.05 / $0.625 / $1 / $2.50)로 요청 전체에 적용됩니다. qgrid는 GPT-6에 쓰는 요청 전체 long-context 규칙으로 이를 계산합니다. adaptive thinking이 기본으로 켜져 있고 API 기본 effort는 `medium`이며, API는 `xhigh`/`max`에서 thinking 비활성화를 거부하고 Claude Code 2.1.293 카탈로그도 `rejects_disabled_thinking`으로 표시하므로 qgrid는 adaptive thinking을 유지하고 `effort`(SDK 기본 `low`)로 추론 깊이를 조절합니다. Haiku 5.5는 Claude 4.7+ 토크나이저를 쓰므로 같은 텍스트가 Haiku 4.5보다 약 30% 더 많은 토큰으로 계산됩니다.
437
442
 
438
443
  Claude Code는 Fable의 safety refusal을 다른 Opus 모델로 자동 재시도할 수 있습니다. 현재 CLI는 refusal 카테고리에 따라 Opus 5 또는 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 토큰 분해도 확인할 수 있습니다.
439
444
 
@@ -489,6 +494,21 @@ qgrid(modelId, {
489
494
  한도인 64 KiB도 넘지 않아야 합니다.
490
495
  - AI SDK의 `toolChoice`는 현재 qgrid에서 지원하지 않습니다.
491
496
 
497
+ ## 저장소 테스트
498
+
499
+ `mise run test`는 provider 계정·DB 없이 API·SDK·CLI 테스트를 실행합니다. OAuth relay 테스트는 로컬 loopback 포트를 사용합니다. DB 통합 검증은 PostgreSQL 테스트 템플릿을 준비한 뒤 `mise exec -- pnpm --dir packages/api test:db`로 별도 실행합니다. API의 `test` 명령은 두 suite를 모두 실행합니다.
500
+
501
+ 실제 provider E2E는 토큰이 등록된 격리 qgrid 서버와 명시적인 opt-in이 필요합니다.
502
+
503
+ ```bash
504
+ QGRID_REAL_PROVIDER_ACCEPTANCE=1 QGRID_URL=http://localhost:44900 QGRID_MODEL=openai/gpt-6.1-sol \
505
+ mise exec -- pnpm --dir packages/ai-sdk e2e
506
+ ```
507
+
508
+ 기본 suite는 일반 응답·스트리밍·툴+구조화 출력과 반환된 ID의 요청 로그를 확인합니다. Anthropic 경로는 해당 모델을 지정하고, 두 경로를 모두 검사하려면 모델을 쉼표로 나열합니다. `e2e:tools-output`은 구조화 출력의 집중 회귀 검증, `e2e:logger`는 telemetry가 native 로그를 중복 생성하지 않는지 확인합니다. 모두 같은 opt-in·서버·모델 설정을 요구하며 실제 쿼터를 사용합니다. 기본 CI에는 포함하지 않고, 실패를 자동 재시도로 감추거나 서버 로그를 삭제하지 않습니다.
509
+
510
+ 테스트는 외부에서 관측되는 계약이나 구체적인 회귀 위험에 한해 추가합니다. 실패 경로·비용·데이터 무결성 검증은 유지하고, 중복 정상 경로를 통합합니다. 새 범용 harness는 만들지 않습니다. DB·provider 환경이 없으면 성공이 아니라 미검증으로 기록합니다.
511
+
492
512
  ## 요구사항
493
513
 
494
514
  - Node.js >= 20
package/README.md CHANGED
@@ -221,7 +221,7 @@ const { text } = await generateText({
221
221
  | `timeoutMs` | positive integer, max `1_800_000` | Anthropic only | Server-side Claude Code process timeout in milliseconds. The SDK's non-stream HTTP budget is 60 seconds longer. Defaults to 240 seconds |
222
222
  | `imageGeneration` | `boolean` | OpenAI only, non-stream | Enables codex's built-in `image_generation` tool (see [below](#image-generation)) |
223
223
  | `imageGenerationOptions` | `{ quality?, size?, background?, outputFormat? }` | OpenAI only | Image controls; `outputFormat` selects PNG/JPEG/WebP on Responses. `background: "transparent"` selects standalone Images (PNG only). Read actual size/quality from response metadata ([below](#image-generation)). |
224
- | `fallbackModels` | `string[]` | reserved | Reserved for future qgrid server-side fallback routing. Not functional yet and unrelated to Claude Code's Fable refusal fallback |
224
+ | `fallbackModels` | `string[]` | deprecated | Ignored compatibility field; emits an unsupported-option warning. Remove it from callers. Unrelated to Claude Code's Fable refusal fallback |
225
225
 
226
226
  ```typescript
227
227
  await generateText({
@@ -335,6 +335,8 @@ Remove green-screen or other background instructions when requesting transparenc
335
335
 
336
336
  ## Telemetry Logger
337
337
 
338
+ 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.
339
+
338
340
  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`.
339
341
 
340
342
  ```typescript
@@ -434,6 +436,7 @@ type QgridSupportedModel =
434
436
  | "anthropic/claude-opus-5"
435
437
  | "anthropic/claude-opus-5-5"
436
438
  | "anthropic/claude-sonnet-5-5"
439
+ | "anthropic/claude-haiku-5-5"
437
440
  ```
438
441
 
439
442
  `openai/gpt-5.4`, `openai/gpt-5.4-mini`, `openai/gpt-5.2`, and `openai/gpt-5.3-codex` remain in the type for backward compatibility, but the ChatGPT-subscription Codex route that qgrid uses no longer serves them. `gpt-5.4` and `gpt-5.4-mini` retired on 2026-08-31 (replacements: `openai/gpt-5.6-terra` and `openai/gpt-5.6-luna`); `gpt-5.2` and `gpt-5.3-codex` were removed from that route earlier. Requests for these ids fail at the backend.
@@ -469,7 +472,9 @@ Qgrid uses [Standard API pricing](https://developers.openai.com/api/docs/pricing
469
472
 
470
473
  `anthropic/claude-opus-5-5` (released 2026-09-22) has a 1M context window and 128K max output. Its prices per 1M tokens are $4 input, $0.20 cache read (0.05x the input price), $5 five-minute cache write, $8 one-hour cache write, and $20 output. Adaptive thinking is always on and cannot be disabled, so qgrid preserves it and uses `effort` to control depth, as it does for Fable. Opus 5.5's API-level breaking changes (forced `tool_choice` rejection, model-bound thinking blocks) do not affect qgrid for the same reason as Fable 5.1. Opus 5 pricing did not change.
471
474
 
472
- `anthropic/claude-sonnet-5-5` (released 2026-09-29) has a 1M context window and 128K max output at Sonnet 5's prices: $2 input, $0.20 cache read, $2.50 five-minute cache write, $4 one-hour cache write, and $10 output per 1M tokens. It rejects disabled thinking, so qgrid keeps adaptive thinking on and uses `effort` to control depth, as it does for Opus 5.5.
475
+ `anthropic/claude-sonnet-5-5` (released 2026-09-28) has a 1M context window and 128K max output at Sonnet 5's base prices: $2 input, $2.50 five-minute cache write, $4 one-hour cache write, and $10 output per 1M tokens. Its cache reads cost $0.10 (0.05x the input price, the same multiplier as Opus 5.5) since Anthropic halved them from $0.20 on 2026-10-07; qgrid prices each request at the rate current when it runs, so logs from before that date keep the $0.20 estimate. It rejects disabled thinking, so qgrid keeps adaptive thinking on and uses `effort` to control depth, as it does for Opus 5.5.
476
+
477
+ `anthropic/claude-haiku-5-5` (released 2026-10-07) has a 1M context window and 128K max output, and is the only current Claude model priced by prompt length. For prompts up to 100K tokens it costs $0.10 input, $0.01 cache read, $0.125 five-minute cache write, $0.20 one-hour cache write, and $0.50 output per 1M tokens; once the prompt exceeds 100K tokens, every rate is 5x ($0.50 / $0.05 / $0.625 / $1 / $2.50) and applies to the whole request, which qgrid models with the same whole-request long-context rule it uses for GPT-6. Adaptive thinking is on by default with a `medium` API default effort; the API rejects disabled thinking at `xhigh` and `max`, and Claude Code 2.1.293 marks the model `rejects_disabled_thinking`, so qgrid keeps adaptive thinking on and uses `effort` (SDK default `low`) to control depth. Haiku 5.5 uses the Claude 4.7+ tokenizer, so the same text counts roughly 30% more tokens than on Haiku 4.5.
473
478
 
474
479
  Claude Code may automatically retry a Fable safety refusal on another Opus model; the current CLI picks Opus 5 or Opus 4.8 by refusal category. 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.
475
480
 
@@ -528,6 +533,21 @@ If multiple projects/workflows share one qgrid server, set `QGRID_PROJECT_NAME`.
528
533
  single-argument budget used by the Claude Code transport.
529
534
  - AI SDK `toolChoice` is not currently supported by qgrid.
530
535
 
536
+ ## Repository testing
537
+
538
+ Run `mise run test` for DB-free API, SDK, and CLI tests. The OAuth relay test uses a local loopback port; no provider credentials are needed. Run `mise exec -- pnpm --dir packages/api test:db` separately with the prepared PostgreSQL test template. The API's `test` command still runs both suites.
539
+
540
+ Real-provider E2E is opt-in and requires an isolated qgrid server with registered tokens:
541
+
542
+ ```bash
543
+ QGRID_REAL_PROVIDER_ACCEPTANCE=1 QGRID_URL=http://localhost:44900 QGRID_MODEL=openai/gpt-6.1-sol \
544
+ mise exec -- pnpm --dir packages/ai-sdk e2e
545
+ ```
546
+
547
+ The default suite checks text, streaming, and tool-plus-structured output, following returned request-log IDs to persisted results. Set an Anthropic model for that route, or use a comma-separated model list to explicitly run both. `e2e:tools-output` is a focused structured-output regression probe; `e2e:logger` checks native logging is not duplicated by telemetry. All live commands consume quota and require the same explicit opt-in/server/model settings. They are not part of ordinary CI, do not retry failures automatically, and do not delete server logs.
548
+
549
+ Keep new tests focused on observable contracts or concrete regressions. Preserve failure-path, cost, and data-integrity checks; consolidate duplicate happy paths instead of adding more mocks or a new test harness. Missing DB/provider infrastructure means unverified, not passed.
550
+
531
551
  ## Requirements
532
552
 
533
553
  - Node.js >= 20
package/dist/index.d.ts CHANGED
@@ -39,8 +39,8 @@ type QgridCommonProviderOptions = {
39
39
  */
40
40
  logger?: boolean;
41
41
  /**
42
- * @todo 향후 qgrid 서버 fallback routing에 사용할 후보 모델 목록.
43
- * Claude Code가 처리하는 Fable safety-refusal fallback과는 무관하다.
42
+ * @deprecated Ignored: qgrid does not implement caller-configured model fallback routing.
43
+ * Retained for source compatibility. Claude Code's safety fallback is independent.
44
44
  */
45
45
  fallbackModels?: string[];
46
46
  };
@@ -96,7 +96,7 @@ type QgridAnthropicProviderOptions = QgridCommonProviderOptions & {
96
96
  * `QgridOpenAIProviderOptions` / `QgridAnthropicProviderOptions` 를 직접 쓰는 편이 정확하다.
97
97
  */
98
98
  type QgridProviderOptions = QgridOpenAIProviderOptions | QgridAnthropicProviderOptions;
99
- type QgridSupportedModel = "openai/gpt-6-astra" | "openai/gpt-6.1-sol" | "openai/gpt-6-sol" | "openai/gpt-6-luna" | "openai/gpt-5.6-sol" | "openai/gpt-5.6-terra" | "openai/gpt-5.6-luna" | "openai/gpt-5.5" | "openai/gpt-5.4" | "openai/gpt-5.2" | "openai/gpt-5.4-mini" | "openai/gpt-5.3-codex" | "openai/gpt-5.3-codex-spark" | "anthropic/claude-fable-5-1" | "anthropic/claude-fable-5" | "anthropic/claude-haiku-4-5" | "anthropic/claude-sonnet-4" | "anthropic/claude-sonnet-4-5" | "anthropic/claude-sonnet-4-6" | "anthropic/claude-sonnet-4-7" | "anthropic/claude-sonnet-5" | "anthropic/claude-opus-4" | "anthropic/claude-opus-4-1" | "anthropic/claude-opus-4-5" | "anthropic/claude-opus-4-6" | "anthropic/claude-opus-4-7" | "anthropic/claude-opus-4-8" | "anthropic/claude-opus-5" | "anthropic/claude-opus-5-5" | "anthropic/claude-sonnet-5-5";
99
+ type QgridSupportedModel = "openai/gpt-6-astra" | "openai/gpt-6.1-sol" | "openai/gpt-6-sol" | "openai/gpt-6-luna" | "openai/gpt-5.6-sol" | "openai/gpt-5.6-terra" | "openai/gpt-5.6-luna" | "openai/gpt-5.5" | "openai/gpt-5.4" | "openai/gpt-5.2" | "openai/gpt-5.4-mini" | "openai/gpt-5.3-codex" | "openai/gpt-5.3-codex-spark" | "anthropic/claude-fable-5-1" | "anthropic/claude-fable-5" | "anthropic/claude-haiku-4-5" | "anthropic/claude-sonnet-4" | "anthropic/claude-sonnet-4-5" | "anthropic/claude-sonnet-4-6" | "anthropic/claude-sonnet-4-7" | "anthropic/claude-sonnet-5" | "anthropic/claude-opus-4" | "anthropic/claude-opus-4-1" | "anthropic/claude-opus-4-5" | "anthropic/claude-opus-4-6" | "anthropic/claude-opus-4-7" | "anthropic/claude-opus-4-8" | "anthropic/claude-opus-5" | "anthropic/claude-opus-5-5" | "anthropic/claude-sonnet-5-5" | "anthropic/claude-haiku-5-5";
100
100
  /** Observed image response values. Usage is response-level and appears on the first image only. */
101
101
  type QgridImageGenerationMetadata = {
102
102
  route: "codex-images";
package/dist/index.js CHANGED
@@ -396,27 +396,41 @@ function extractToolResultsFromHistory(messages) {
396
396
  });
397
397
  return out;
398
398
  }
399
- async function* parseSSE(body) {
399
+ async function* parseSSE(body, signal) {
400
400
  const reader = body.getReader();
401
401
  const decoder = new TextDecoder();
402
402
  let buffer = "";
403
403
  let eventType = "";
404
- for (;;) {
405
- const { done, value } = await reader.read();
406
- if (done) break;
407
- buffer += decoder.decode(value, { stream: true });
408
- const lines = buffer.split("\n");
409
- buffer = lines.pop() ?? "";
410
- for (const line of lines) if (line.startsWith("event: ")) eventType = line.slice(7).trim();
411
- else if (line.startsWith("data: ")) {
412
- const raw = line.slice(6);
413
- try {
414
- yield {
415
- type: eventType || "message",
416
- data: JSON.parse(raw)
417
- };
418
- } catch {}
419
- eventType = "";
404
+ const cancel = () => {
405
+ reader.cancel(signal?.reason).catch(() => void 0);
406
+ };
407
+ signal?.addEventListener("abort", cancel, { once: true });
408
+ try {
409
+ signal?.throwIfAborted();
410
+ for (;;) {
411
+ const { done, value } = await reader.read();
412
+ if (done) break;
413
+ buffer += decoder.decode(value, { stream: true });
414
+ const lines = buffer.split("\n");
415
+ buffer = lines.pop() ?? "";
416
+ for (const line of lines) if (line.startsWith("event: ")) eventType = line.slice(7).trim();
417
+ else if (line.startsWith("data: ")) {
418
+ const raw = line.slice(6);
419
+ try {
420
+ yield {
421
+ type: eventType || "message",
422
+ data: JSON.parse(raw)
423
+ };
424
+ } catch {}
425
+ eventType = "";
426
+ }
427
+ }
428
+ } finally {
429
+ signal?.removeEventListener("abort", cancel);
430
+ try {
431
+ await reader.cancel();
432
+ } catch {} finally {
433
+ reader.releaseLock();
420
434
  }
421
435
  }
422
436
  }
@@ -1029,6 +1043,7 @@ function createQgridLogger(config = {}) {
1029
1043
  //#region src/index.ts
1030
1044
  const threadCoordStore = /* @__PURE__ */ new Map();
1031
1045
  const CACHE_AFFINITY_NAMESPACE = "qgrid-cache-affinity-v1";
1046
+ const MAX_THREAD_COORDS = 1e3;
1032
1047
  const MAX_IMAGE_INPUT_DATA_URL_CHARS = 9e6;
1033
1048
  const MAX_IMAGE_INPUT_DATA_URL_CHARS_LABEL = "9M";
1034
1049
  function assertImageInputFitsJsonTransport(imageUrls) {
@@ -1064,10 +1079,21 @@ function getThreadCoord(sessionKey) {
1064
1079
  return entry.coord;
1065
1080
  }
1066
1081
  function setThreadCoord(sessionKey, coord) {
1082
+ const now = Date.now();
1083
+ for (const [key, entry] of threadCoordStore) if (entry.expiresAt <= now) threadCoordStore.delete(key);
1084
+ threadCoordStore.delete(sessionKey);
1067
1085
  threadCoordStore.set(sessionKey, {
1068
1086
  coord,
1069
- expiresAt: Date.now() + THREAD_COORD_TTL_MS
1087
+ expiresAt: now + THREAD_COORD_TTL_MS
1070
1088
  });
1089
+ if (threadCoordStore.size > MAX_THREAD_COORDS) threadCoordStore.delete(threadCoordStore.keys().next().value);
1090
+ }
1091
+ function providerWarnings(options) {
1092
+ return options.fallbackModels === void 0 ? [] : [{
1093
+ type: "unsupported",
1094
+ feature: "providerOptions.qgrid.fallbackModels",
1095
+ details: "fallbackModels is deprecated and ignored; qgrid does not implement model fallback routing."
1096
+ }];
1071
1097
  }
1072
1098
  async function deriveCacheAffinityKey(modelId, sessionKey) {
1073
1099
  if (!sessionKey || modelId.startsWith("anthropic/")) return void 0;
@@ -1081,6 +1107,7 @@ function qgridProviderMetadata(data) {
1081
1107
  ...item.generation
1082
1108
  }] : []);
1083
1109
  return { qgrid: {
1110
+ ...data.requestLogId !== void 0 ? { requestLogId: data.requestLogId } : {},
1084
1111
  model: data.model,
1085
1112
  requestedModel: data.requestedModel ?? data.model,
1086
1113
  modelFallbacks: data.modelFallbacks ?? [],
@@ -1263,7 +1290,7 @@ function qgrid(modelId, config) {
1263
1290
  content,
1264
1291
  finishReason,
1265
1292
  usage: toAiSdkUsage(data.usage),
1266
- warnings: [],
1293
+ warnings: providerWarnings(qgridOptions),
1267
1294
  providerMetadata: qgridProviderMetadata(data),
1268
1295
  response: { modelId: data.model }
1269
1296
  };
@@ -1294,6 +1321,8 @@ function qgrid(modelId, config) {
1294
1321
  }
1295
1322
  const storedCoord = cacheAffinityKey ? getThreadCoord(cacheAffinityKey) : void 0;
1296
1323
  if (!matchedClientRun && storedCoord) runContext = { threadCoord: storedCoord };
1324
+ const abortController = new AbortController();
1325
+ const signal = options.abortSignal ? AbortSignal.any([options.abortSignal, abortController.signal]) : abortController.signal;
1297
1326
  const prepRes = await fetch(`${serverUrl}/api/qgrid/prepareStream`, {
1298
1327
  method: "POST",
1299
1328
  headers: { "Content-Type": "application/json" },
@@ -1316,14 +1345,14 @@ function qgrid(modelId, config) {
1316
1345
  ...runContext ? { runContext } : {},
1317
1346
  ...toolResultsPayload ? { toolResults: toolResultsPayload } : {}
1318
1347
  } }),
1319
- signal: options.abortSignal
1348
+ signal
1320
1349
  });
1321
1350
  if (!prepRes.ok) {
1322
1351
  const text = await prepRes.text().catch(() => "");
1323
1352
  throw new Error(`qgrid prepareStream ${prepRes.status}: ${text}`);
1324
1353
  }
1325
1354
  const { streamId } = await prepRes.json();
1326
- const streamRes = await fetch(`${serverUrl}/api/qgrid/queryStream?streamId=${streamId}`, { signal: options.abortSignal });
1355
+ const streamRes = await fetch(`${serverUrl}/api/qgrid/queryStream?streamId=${streamId}`, { signal });
1327
1356
  if (!streamRes.ok || !streamRes.body) {
1328
1357
  const text = await streamRes.text().catch(() => "");
1329
1358
  throw new Error(`qgrid stream ${streamRes.status}: ${text}`);
@@ -1332,107 +1361,131 @@ function qgrid(modelId, config) {
1332
1361
  let textStarted = false;
1333
1362
  let deltaTextEmitted = false;
1334
1363
  const envelopeParser = hasTools ? createEnvelopeStreamParser(jsonSchema ? "json" : "text") : void 0;
1335
- return { stream: new ReadableStream({ async start(controller) {
1364
+ let cancelled = false;
1365
+ let processing;
1366
+ const stream = new ReadableStream({
1367
+ start(controller) {
1368
+ processing = processEvents(controller);
1369
+ },
1370
+ async cancel(reason) {
1371
+ cancelled = true;
1372
+ abortController.abort(reason);
1373
+ await processing;
1374
+ }
1375
+ });
1376
+ async function processEvents(controller) {
1336
1377
  try {
1378
+ controller.enqueue({
1379
+ type: "stream-start",
1380
+ warnings: providerWarnings(qgridOptions)
1381
+ });
1337
1382
  let streamCompleted = false;
1338
- for await (const event of parseSSE(streamRes.body)) if (event.type === "delta") {
1339
- const raw = event.data.text;
1340
- const text = envelopeParser ? envelopeParser.push(raw) : raw;
1341
- if (text !== "") {
1342
- if (!textStarted) {
1383
+ for await (const event of parseSSE(streamRes.body, signal)) {
1384
+ if (cancelled) return;
1385
+ signal.throwIfAborted();
1386
+ if (event.type === "delta") {
1387
+ const raw = event.data.text;
1388
+ const text = envelopeParser ? envelopeParser.push(raw) : raw;
1389
+ if (text !== "") {
1390
+ if (!textStarted) {
1391
+ controller.enqueue({
1392
+ type: "text-start",
1393
+ id: textId
1394
+ });
1395
+ textStarted = true;
1396
+ }
1343
1397
  controller.enqueue({
1344
- type: "text-start",
1398
+ type: "text-delta",
1399
+ id: textId,
1400
+ delta: text
1401
+ });
1402
+ deltaTextEmitted = true;
1403
+ }
1404
+ } else if (event.type === "done") {
1405
+ if (textStarted) {
1406
+ controller.enqueue({
1407
+ type: "text-end",
1345
1408
  id: textId
1346
1409
  });
1347
- textStarted = true;
1410
+ textStarted = false;
1411
+ }
1412
+ const done = event.data;
1413
+ if (matchedClientRun) clientRuns.delete(matchedClientRun.key);
1414
+ if (done.finishReason === "tool-calls") rememberClientRun(done.runContext, (done.content ?? []).filter((c) => c.type === "tool-call").map((c) => c.toolCallId));
1415
+ if (cacheAffinityKey && done.runContext?.threadCoord) setThreadCoord(cacheAffinityKey, done.runContext.threadCoord);
1416
+ if (done.content) {
1417
+ for (const item of done.content) if (item.type === "text" && !deltaTextEmitted) {
1418
+ const tid = `${TEXT_STREAM_ID_PREFIX}_${Math.random().toString(36).slice(2, 10)}`;
1419
+ controller.enqueue({
1420
+ type: "text-start",
1421
+ id: tid
1422
+ });
1423
+ controller.enqueue({
1424
+ type: "text-delta",
1425
+ id: tid,
1426
+ delta: item.text
1427
+ });
1428
+ controller.enqueue({
1429
+ type: "text-end",
1430
+ id: tid
1431
+ });
1432
+ } else if (item.type === "tool-call") {
1433
+ controller.enqueue({
1434
+ type: "tool-input-start",
1435
+ id: item.toolCallId,
1436
+ toolName: item.toolName
1437
+ });
1438
+ controller.enqueue({
1439
+ type: "tool-input-delta",
1440
+ id: item.toolCallId,
1441
+ delta: item.input
1442
+ });
1443
+ controller.enqueue({
1444
+ type: "tool-input-end",
1445
+ id: item.toolCallId
1446
+ });
1447
+ controller.enqueue({
1448
+ type: "tool-call",
1449
+ toolCallId: item.toolCallId,
1450
+ toolName: item.toolName,
1451
+ input: item.input
1452
+ });
1453
+ }
1348
1454
  }
1349
1455
  controller.enqueue({
1350
- type: "text-delta",
1351
- id: textId,
1352
- delta: text
1456
+ type: "response-metadata",
1457
+ modelId: done.model
1353
1458
  });
1354
- deltaTextEmitted = true;
1355
- }
1356
- } else if (event.type === "done") {
1357
- if (textStarted) {
1358
1459
  controller.enqueue({
1359
- type: "text-end",
1360
- id: textId
1460
+ type: "finish",
1461
+ finishReason: done.finishReason === "tool-calls" ? {
1462
+ unified: "tool-calls",
1463
+ raw: "tool_call"
1464
+ } : {
1465
+ unified: "stop",
1466
+ raw: "stop"
1467
+ },
1468
+ usage: toAiSdkUsage(done.usage),
1469
+ providerMetadata: qgridProviderMetadata(done)
1361
1470
  });
1362
- textStarted = false;
1471
+ streamCompleted = true;
1472
+ controller.close();
1473
+ return;
1474
+ } else if (event.type === "error") {
1475
+ streamCompleted = true;
1476
+ controller.error(new Error(event.data.message));
1477
+ return;
1363
1478
  }
1364
- const done = event.data;
1365
- if (matchedClientRun) clientRuns.delete(matchedClientRun.key);
1366
- if (done.finishReason === "tool-calls") rememberClientRun(done.runContext, (done.content ?? []).filter((c) => c.type === "tool-call").map((c) => c.toolCallId));
1367
- if (cacheAffinityKey && done.runContext?.threadCoord) setThreadCoord(cacheAffinityKey, done.runContext.threadCoord);
1368
- if (done.content) {
1369
- for (const item of done.content) if (item.type === "text" && !deltaTextEmitted) {
1370
- const tid = `${TEXT_STREAM_ID_PREFIX}_${Math.random().toString(36).slice(2, 10)}`;
1371
- controller.enqueue({
1372
- type: "text-start",
1373
- id: tid
1374
- });
1375
- controller.enqueue({
1376
- type: "text-delta",
1377
- id: tid,
1378
- delta: item.text
1379
- });
1380
- controller.enqueue({
1381
- type: "text-end",
1382
- id: tid
1383
- });
1384
- } else if (item.type === "tool-call") {
1385
- controller.enqueue({
1386
- type: "tool-input-start",
1387
- id: item.toolCallId,
1388
- toolName: item.toolName
1389
- });
1390
- controller.enqueue({
1391
- type: "tool-input-delta",
1392
- id: item.toolCallId,
1393
- delta: item.input
1394
- });
1395
- controller.enqueue({
1396
- type: "tool-input-end",
1397
- id: item.toolCallId
1398
- });
1399
- controller.enqueue({
1400
- type: "tool-call",
1401
- toolCallId: item.toolCallId,
1402
- toolName: item.toolName,
1403
- input: item.input
1404
- });
1405
- }
1406
- }
1407
- controller.enqueue({
1408
- type: "response-metadata",
1409
- modelId: done.model
1410
- });
1411
- controller.enqueue({
1412
- type: "finish",
1413
- finishReason: done.finishReason === "tool-calls" ? {
1414
- unified: "tool-calls",
1415
- raw: "tool_call"
1416
- } : {
1417
- unified: "stop",
1418
- raw: "stop"
1419
- },
1420
- usage: toAiSdkUsage(done.usage),
1421
- providerMetadata: qgridProviderMetadata(done)
1422
- });
1423
- streamCompleted = true;
1424
- controller.close();
1425
- return;
1426
- } else if (event.type === "error") {
1427
- streamCompleted = true;
1428
- controller.error(new Error(event.data.message));
1429
- return;
1430
1479
  }
1431
- if (!streamCompleted) controller.error(/* @__PURE__ */ new Error("qgrid stream ended unexpectedly"));
1480
+ if (!streamCompleted && !cancelled) {
1481
+ signal.throwIfAborted();
1482
+ controller.error(/* @__PURE__ */ new Error("qgrid stream ended unexpectedly"));
1483
+ }
1432
1484
  } catch (e) {
1433
- controller.error(e);
1485
+ if (!cancelled) controller.error(e);
1434
1486
  }
1435
- } }) };
1487
+ }
1488
+ return { stream };
1436
1489
  }
1437
1490
  };
1438
1491
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartanova/qgrid-ai-sdk",
3
- "version": "2.9.10",
3
+ "version": "2.10.1",
4
4
  "description": "AI SDK LanguageModelV3 provider for qgrid",
5
5
  "repository": {
6
6
  "type": "git",