@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 +28 -1
- package/README.md +28 -1
- package/dist/index.d.ts +6 -8
- package/dist/index.js +90 -33
- package/package.json +4 -4
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는 이
|
|
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
|
|
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
|
|
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 (
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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:
|
|
462
|
+
errorMessage: OVERLAPPING_RUN_ERROR
|
|
449
463
|
});
|
|
450
|
-
|
|
451
|
-
|
|
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:
|
|
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 (
|
|
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 (
|
|
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 (
|
|
586
|
-
|
|
641
|
+
if (suppressedRuns.has(runKey)) {
|
|
642
|
+
suppressedRuns.remove(runKey);
|
|
587
643
|
return;
|
|
588
644
|
}
|
|
589
645
|
if (quarantined.has(runKey)) return;
|
|
590
|
-
|
|
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
|
|
679
|
-
if (runContext
|
|
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 (!
|
|
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
|
-
...
|
|
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 (
|
|
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 (!
|
|
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
|
-
...
|
|
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.
|
|
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.
|
|
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.
|
|
25
|
+
"tsdown": "^0.22.3",
|
|
26
26
|
"typescript": "^5.9.3",
|
|
27
|
-
"vitest": "^4.
|
|
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
|
|
34
|
+
"build": "tsdown",
|
|
35
35
|
"test": "vitest run",
|
|
36
36
|
"e2e": "tsx e2e/e2e.ts",
|
|
37
37
|
"e2e:logger": "tsx e2e/e2e-logger.ts"
|