@cartanova/qgrid-ai-sdk 2.5.3 → 2.5.4

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
@@ -93,7 +93,7 @@ console.log(output.title, output.authors);
93
93
  top-level이 `object`인 schema는 서버 structured output으로 전달되어 강제됩니다.
94
94
  top-level이 `object`가 아니면 (예: array) AI SDK 클라이언트 파싱으로 fallback되며 경고 로그가 출력됩니다.
95
95
 
96
- > **Anthropic 모델 주의:** OpenAI/codex structured output은 디코딩 단계에서 schema를 강제(constrained decoding)하므로 이 부류 실패가 거의 없지만, Claude Code의 `--json-schema`는 StructuredOutput tool + 사후 검증 방식이라 복잡한 schema는 모델이 준수하지 못할 수 있습니다. qgrid는 Claude Code의 내부 재시도를 기본 차단하고(`MAX_STRUCTURED_OUTPUT_RETRIES=1` = 1회 시도, 1 미만 값은 1로 클램프) 검증 실패 시 깨진 JSON 대신 명시적 에러를 반환합니다. 서버 환경변수로 값을 올려 재시도를 허용할 수 있지만, 재시도 시 누적되는 rejected 출력이 스트리밍 경로의 AI SDK 파싱을 깨뜨릴 수 있어 기본값 유지를 권장합니다.
96
+ > **Anthropic 모델 주의:** OpenAI/codex structured output은 디코딩 단계에서 schema를 강제(constrained decoding)하므로 이 부류 실패가 거의 없지만, Claude Code의 `--json-schema`는 `StructuredOutput` tool + 사후 검증 방식이라 복잡한 schema는 모델이 준수하지 못할 수 있습니다. qgrid는 structured streaming에만 `MAX_STRUCTURED_OUTPUT_RETRIES=1`을 주입해 1회 시도로 제한합니다(1 미만 값은 1로 클램프). non-stream `generate`에는 이 override를 주입하지 않고 Claude Code의 기본 retry 예산을 사용합니다. 각 경로의 시도 이후 검증이 실패하면 깨진 JSON 대신 명시적 에러를 반환합니다.
97
97
 
98
98
  ### 스트리밍
99
99
 
@@ -114,7 +114,7 @@ for await (const chunk of textStream) {
114
114
  ### Tool Calling
115
115
 
116
116
  ```typescript
117
- import { generateText, tool } from "ai";
117
+ import { generateText, stepCountIs, tool } from "ai";
118
118
  import { qgrid } from "@cartanova/qgrid-ai-sdk";
119
119
  import { z } from "zod";
120
120
 
@@ -124,17 +124,54 @@ const { text } = await generateText({
124
124
  tools: {
125
125
  getWeather: tool({
126
126
  description: "도시의 현재 날씨 조회",
127
- parameters: z.object({ city: z.string() }),
127
+ inputSchema: z.object({ city: z.string() }),
128
128
  execute: async ({ city }) => {
129
129
  return { temperature: 22, condition: "맑음" };
130
130
  },
131
131
  }),
132
132
  },
133
+ stopWhen: stepCountIs(3),
133
134
  });
134
135
  ```
135
136
 
136
137
  tool-call은 qgrid 서버의 structured output emulation으로 동작합니다.
137
138
  AI SDK가 tool 실행을 관리하고, qgrid는 각 턴의 LLM 호출만 담당합니다.
139
+ 실행 가능한 tool을 쓸 때는 제한이 있는 `stopWhen`을 지정해야 AI SDK가
140
+ tool-call 턴 뒤에도 계속 진행해 모델의 최종 응답을 받습니다.
141
+
142
+ `tools`와 `Output.object`를 함께 사용할 수도 있습니다.
143
+
144
+ ```typescript
145
+ import { generateText, Output, stepCountIs, tool } from "ai";
146
+ import { qgrid } from "@cartanova/qgrid-ai-sdk";
147
+ import { z } from "zod";
148
+
149
+ const { output } = await generateText({
150
+ model: qgrid("openai/gpt-5.4"),
151
+ prompt: "서울 날씨를 조회해서 예보를 반환해줘.",
152
+ tools: {
153
+ getWeather: tool({
154
+ description: "도시의 현재 날씨 조회",
155
+ inputSchema: z.object({ city: z.string() }),
156
+ execute: async ({ city }) => ({ city, temperature: 22 }),
157
+ }),
158
+ },
159
+ stopWhen: stepCountIs(3),
160
+ output: Output.object({
161
+ schema: z.object({
162
+ city: z.string(),
163
+ summary: z.string(),
164
+ }),
165
+ }),
166
+ });
167
+ ```
168
+
169
+ qgrid 2.5.4는 모든 모델 턴에 합성된 action envelope를 강제합니다. tool-call
170
+ 턴은 AI SDK tool call로 유지되고, 마지막 `answer`는 사용자 schema로 강제된 뒤
171
+ `output`으로 반환됩니다. 제한이 있는 `stopWhen`을 지정하지 않으면 AI SDK가
172
+ 기본 첫 번째 step에서 멈춰 최종 structured output을 만들 수 없습니다. qgrid server
173
+ 2.5.4와 `@cartanova/qgrid-ai-sdk` 2.5.4가 모두 필요합니다. AI SDK의
174
+ `toolChoice`는 현재 전송하거나 강제하지 않으며 tool 선택은 모델이 결정합니다.
138
175
 
139
176
  ### Provider Options
140
177
 
@@ -369,7 +406,30 @@ qgrid(modelId, {
369
406
 
370
407
  - `temperature`, `maxOutputTokens` 등 sampling 파라미터는 CLI 런타임(OpenAI: codex app-server, Anthropic: Claude Code)이 지원하지 않아 무시됩니다.
371
408
  - Structured output은 top-level `object` schema만 서버에서 강제됩니다. top-level `array`는 클라이언트 파싱 fallback.
372
- - `tools`와 structured output schema는 서버에서 동시에 사용할 수 없습니다. tools가 있으면 schema는 서버로 전달되지 않고 최종 텍스트를 AI SDK가 클라이언트에서 파싱합니다.
409
+ - `tools`와 `Output.object`를 함께 사용하려면 qgrid server와 AI SDK가 모두 2.5.4 이상이어야 합니다.
410
+ - AI SDK/Zod가 Draft-7 `items: [...]`로 만드는 위치 기반 tuple은 지원되는
411
+ positive schema 위치에서 OpenAI 호출 전에 정규화되고 위치별 제약이
412
+ 강제됩니다. tuple tail이 생략되면 고정 길이로 해석하며,
413
+ `additionalItems: true`로 명시한 무제한 tail은 HTTP 400으로 거부합니다.
414
+ negative, conditional 등 안전하게 정규화할 수 없는 위치의 tuple도 의미가
415
+ 바뀌는 변환 대신 HTTP 400으로 거부합니다. definition은 전역 정규화되므로
416
+ 해당 위치에서 참조하는 경우도 같은 이유로 거부합니다. Anthropic 위치 기반
417
+ tuple schema는 Claude Code가 위치 의미를 보존할 수 없어 HTTP 400으로
418
+ 거부됩니다. tuple node는 `type: "array"`를 명시해야 하며 nullable tuple은
419
+ array/null `anyOf`로 표현합니다.
420
+ - structured schema에서는 문서 root 또는 `$defs`/`definitions` entry root만
421
+ 연속으로 가리키는 로컬 root-relative JSON Pointer `$ref`를 허용합니다.
422
+ property, tuple 내부, conditional, literal 값을 가리키는 ref는 정규화 중
423
+ target이 이동하거나 다시 작성될 수 있어 HTTP 400으로 거부합니다. resource
424
+ ID, anchor, 외부 ref, dynamic ref, recursive ref도 허용하지 않습니다.
425
+ - output/tool schema serialization, tool 이름, 설명, JSON escaping,
426
+ composition framing은 합산 UTF-8 512 KiB 전처리 한도를 공유합니다.
427
+ schema 값에는 별도로 합산 20,000 node와 schema별 최대 깊이 128 한도가
428
+ 적용됩니다. 잘못되거나 한도를 넘는 입력은 provider 실행 전에 HTTP 400으로
429
+ 실패합니다.
430
+ - Anthropic 경로에서는 최종 합성 schema가 Claude Code 전송의 안전한 단일 argv
431
+ 한도인 64 KiB도 넘지 않아야 합니다.
432
+ - AI SDK의 `toolChoice`는 현재 qgrid에서 지원하지 않습니다.
373
433
 
374
434
  ## 요구사항
375
435
 
package/README.md CHANGED
@@ -93,7 +93,7 @@ console.log(output.title, output.authors);
93
93
  Schemas with a top-level `object` are forwarded to server-side structured output and enforced.
94
94
  If the top level is not an `object` (e.g. array), the AI SDK falls back to client-side parsing and a warning is logged.
95
95
 
96
- > **Note for Anthropic models:** OpenAI/codex structured output constrains decoding to the schema, so this class of failure is rare there. Claude Code's `--json-schema`, however, works as a StructuredOutput tool + post-hoc validation, so the model may fail to honor a complex schema. qgrid disables Claude Code's internal retries by default (`MAX_STRUCTURED_OUTPUT_RETRIES=1` = one attempt; values below 1 are clamped to 1) and returns an explicit error on validation failure instead of broken JSON. You can raise the server env var to allow retries, but rejected attempts accumulating on the streaming path can break AI SDK parsing, so keeping the default is recommended.
96
+ > **Note for Anthropic models:** OpenAI/codex structured output constrains decoding to the schema, so this class of failure is rare there. Claude Code's `--json-schema`, however, works as a `StructuredOutput` tool plus post-hoc validation, so the model may fail to honor a complex schema. For structured streaming only, qgrid sets `MAX_STRUCTURED_OUTPUT_RETRIES=1` (one attempt; values below 1 are clamped to 1) to bound stream latency. Non-streaming `generate` leaves that override unset and uses Claude Code's default retry budget. Validation failures after the applicable attempts return an explicit error instead of broken JSON.
97
97
 
98
98
  ### Streaming
99
99
 
@@ -114,7 +114,7 @@ for await (const chunk of textStream) {
114
114
  ### Tool Calling
115
115
 
116
116
  ```typescript
117
- import { generateText, tool } from "ai";
117
+ import { generateText, stepCountIs, tool } from "ai";
118
118
  import { qgrid } from "@cartanova/qgrid-ai-sdk";
119
119
  import { z } from "zod";
120
120
 
@@ -124,17 +124,55 @@ const { text } = await generateText({
124
124
  tools: {
125
125
  getWeather: tool({
126
126
  description: "Get the current weather for a city",
127
- parameters: z.object({ city: z.string() }),
127
+ inputSchema: z.object({ city: z.string() }),
128
128
  execute: async ({ city }) => {
129
129
  return { temperature: 22, condition: "sunny" };
130
130
  },
131
131
  }),
132
132
  },
133
+ stopWhen: stepCountIs(3),
133
134
  });
134
135
  ```
135
136
 
136
137
  Tool calls work through the qgrid server's structured output emulation.
137
138
  The AI SDK manages tool execution; qgrid handles only each turn's LLM call.
139
+ Executable tools require a bounded `stopWhen` so the AI SDK continues after a
140
+ tool-call step and asks the model for the final response.
141
+
142
+ Tools can be combined with `Output.object`:
143
+
144
+ ```typescript
145
+ import { generateText, Output, stepCountIs, tool } from "ai";
146
+ import { qgrid } from "@cartanova/qgrid-ai-sdk";
147
+ import { z } from "zod";
148
+
149
+ const { output } = await generateText({
150
+ model: qgrid("openai/gpt-5.4"),
151
+ prompt: "Look up Seoul's weather and return a forecast.",
152
+ tools: {
153
+ getWeather: tool({
154
+ description: "Get the current weather for a city",
155
+ inputSchema: z.object({ city: z.string() }),
156
+ execute: async ({ city }) => ({ city, temperature: 22 }),
157
+ }),
158
+ },
159
+ stopWhen: stepCountIs(3),
160
+ output: Output.object({
161
+ schema: z.object({
162
+ city: z.string(),
163
+ summary: z.string(),
164
+ }),
165
+ }),
166
+ });
167
+ ```
168
+
169
+ qgrid 2.5.4 constrains every model turn with a composed action envelope. Tool-call
170
+ turns remain AI SDK tool calls, while the final `answer` is constrained by the
171
+ user schema and returned as `output`. Keep a bounded `stopWhen`; otherwise AI SDK
172
+ stops after its default first step and cannot produce the final structured output.
173
+ This requires both qgrid server 2.5.4 and `@cartanova/qgrid-ai-sdk` 2.5.4. AI SDK
174
+ `toolChoice` is not currently transported or enforced; tool selection remains
175
+ model-driven.
138
176
 
139
177
  ### Provider Options
140
178
 
@@ -370,7 +408,33 @@ If multiple projects/workflows share one qgrid server, set `QGRID_PROJECT_NAME`.
370
408
 
371
409
  - Sampling parameters such as `temperature` and `maxOutputTokens` are ignored — the CLI runtimes (OpenAI: codex app-server, Anthropic: Claude Code) do not support them.
372
410
  - Structured output is server-enforced only for top-level `object` schemas. Top-level `array` falls back to client-side parsing.
373
- - `tools` and a structured output schema cannot be used together on the server. When tools are present, the schema is not forwarded and the AI SDK parses the final text client-side.
411
+ - Combining `tools` with `Output.object` requires qgrid server and AI SDK 2.5.4 or later.
412
+ - AI SDK/Zod positional tuples emitted as Draft-7 `items: [...]` are normalized
413
+ in supported positive schema positions before OpenAI dispatch and their
414
+ positional constraints are enforced. An omitted tuple tail is treated as
415
+ fixed-length; an explicitly unrestricted `additionalItems: true` tail is
416
+ rejected with HTTP 400. Tuples in negative, conditional, or otherwise
417
+ non-normalizable schema positions are also rejected instead of being
418
+ rewritten with changed semantics. References from those positions are
419
+ rejected for the same reason because definitions are normalized globally.
420
+ Anthropic positional tuple schemas are rejected with HTTP 400 because Claude
421
+ Code cannot preserve their positional semantics. Tuple nodes must explicitly
422
+ declare `type: "array"`; express nullable tuples with an `anyOf` array/null
423
+ branch.
424
+ - Structured schemas accept only local root-relative JSON Pointer `$ref` values
425
+ targeting the document root or a chain of `$defs`/`definitions` entry roots.
426
+ References into properties, tuple internals, conditionals, or literal values
427
+ fail with HTTP 400 because normalization can move or rewrite those targets.
428
+ Resource IDs, anchors, external refs, dynamic refs, and recursive refs are
429
+ also rejected.
430
+ - Output/tool schema serialization, tool names, descriptions, JSON escaping,
431
+ and composition framing share an aggregate 512 KiB UTF-8 preprocessing
432
+ budget. Schema values also share a 20,000-node budget and have a maximum
433
+ depth of 128 per schema. Invalid or over-budget inputs fail with HTTP 400
434
+ before provider execution.
435
+ - On Anthropic routes, the final composed schema must also fit the 64 KiB safe
436
+ single-argument budget used by the Claude Code transport.
437
+ - AI SDK `toolChoice` is not currently supported by qgrid.
374
438
 
375
439
  ## Requirements
376
440
 
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  const QGRID_PROVIDER_NAME = "qgrid";
3
3
  const THREAD_COORD_TTL_MS = 10 * 6e4;
4
4
  const TEXT_STREAM_ID_PREFIX = "text";
5
- const TOP_LEVEL_SCHEMA_WARNING = "OpenAI structured output requires \"object\". Falling back to client-side parsing.";
5
+ const TOP_LEVEL_SCHEMA_WARNING = "qgrid server-enforced structured output requires \"object\". Falling back to client-side parsing.";
6
6
  const PENDING_TOOL_RESULTS_WARNING = "[qgrid] tool results did not match any pending qgrid run; sending request without runContext";
7
7
  //#endregion
8
8
  //#region src/utils.ts
@@ -678,6 +678,16 @@ function warnDroppedImages(droppedImageCount) {
678
678
  if (droppedImageCount <= 0) return;
679
679
  console.warn(`[qgrid] ${droppedImageCount} image message part(s) were ignored because providerOptions.qgrid.imageGeneration is not enabled.`);
680
680
  }
681
+ function serializeObjectResponseFormat(responseFormat) {
682
+ const rawSchema = responseFormat?.type === "json" ? responseFormat.schema : void 0;
683
+ if (rawSchema === void 0) return void 0;
684
+ const schemaType = rawSchema.type;
685
+ if (schemaType !== "object") {
686
+ console.warn(`[qgrid] responseFormat.schema top-level type is "${schemaType ?? "unknown"}". ${TOP_LEVEL_SCHEMA_WARNING}`);
687
+ return;
688
+ }
689
+ return JSON.stringify(rawSchema);
690
+ }
681
691
  function getThreadCoord(sessionKey) {
682
692
  const entry = threadCoordStore.get(sessionKey);
683
693
  if (!entry) return void 0;
@@ -788,10 +798,7 @@ function qgrid(modelId, config) {
788
798
  if (imageGeneration) assertImageInputFitsJsonTransport(imageUrls);
789
799
  const sessionKey = qgridOptions.sessionKey;
790
800
  const reuseSessionKey = reusableSessionKey(modelId, sessionKey);
791
- const rawSchema = options.responseFormat?.type === "json" ? options.responseFormat.schema : void 0;
792
- const schemaType = rawSchema ? rawSchema.type : void 0;
793
- if (rawSchema && !hasTools && schemaType !== "object") console.warn(`[qgrid] responseFormat.schema top-level type is "${schemaType ?? "unknown"}". ${TOP_LEVEL_SCHEMA_WARNING}`);
794
- const jsonSchema = !hasTools && rawSchema && schemaType === "object" ? JSON.stringify(rawSchema) : void 0;
801
+ const jsonSchema = serializeObjectResponseFormat(options.responseFormat);
795
802
  let runContext;
796
803
  let toolResultsPayload;
797
804
  const matchedClientRun = matchClientRun(options.prompt);
@@ -889,8 +896,7 @@ function qgrid(modelId, config) {
889
896
  warnDroppedImages(droppedImageCount);
890
897
  const sessionKey = qgridOptions.sessionKey;
891
898
  const reuseSessionKey = reusableSessionKey(modelId, sessionKey);
892
- const rawSchema = options.responseFormat?.type === "json" ? options.responseFormat.schema : void 0;
893
- const jsonSchema = !hasTools && rawSchema && rawSchema.type === "object" ? JSON.stringify(rawSchema) : void 0;
899
+ const jsonSchema = serializeObjectResponseFormat(options.responseFormat);
894
900
  let runContext;
895
901
  let toolResultsPayload;
896
902
  const matchedClientRun = matchClientRun(options.prompt);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartanova/qgrid-ai-sdk",
3
- "version": "2.5.3",
3
+ "version": "2.5.4",
4
4
  "description": "AI SDK LanguageModelV3 provider for qgrid",
5
5
  "repository": {
6
6
  "type": "git",
@@ -24,7 +24,8 @@
24
24
  "ai": "^6.0.190",
25
25
  "tsdown": "^0.22.3",
26
26
  "typescript": "^5.9.3",
27
- "vitest": "^4.1.10"
27
+ "vitest": "^4.1.10",
28
+ "zod": "4.4.3"
28
29
  },
29
30
  "peerDependencies": {
30
31
  "@ai-sdk/provider": "^3.0.0",
@@ -34,6 +35,7 @@
34
35
  "build": "tsdown",
35
36
  "test": "vitest run",
36
37
  "e2e": "tsx e2e/e2e.ts",
38
+ "e2e:tools-output": "tsx e2e/tools-structured-output.ts",
37
39
  "e2e:logger": "tsx e2e/e2e-logger.ts"
38
40
  }
39
41
  }