@cartanova/qgrid-ai-sdk 2.3.7 → 2.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +303 -0
- package/README.md +115 -63
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13 -2
- package/package.json +3 -2
package/README.ko.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# @cartanova/qgrid-ai-sdk
|
|
2
|
+
|
|
3
|
+
[English](./README.md) · **한국어**
|
|
4
|
+
|
|
5
|
+
AI SDK v6 custom `LanguageModelV3` provider for [qgrid](https://github.com/cartanova-ai/Qgrid).
|
|
6
|
+
|
|
7
|
+
**기존 AI SDK 코드 변경 없이, `model` 한 줄만 바꾸면 구독 토큰 풀링(토큰 N개 × worker 병렬 처리) + request log 대시보드를 사용할 수 있습니다.**
|
|
8
|
+
|
|
9
|
+
```diff
|
|
10
|
+
import { generateText } from "ai";
|
|
11
|
+
-import { openai } from "@ai-sdk/openai";
|
|
12
|
+
+import { qgrid } from "@cartanova/qgrid-ai-sdk";
|
|
13
|
+
|
|
14
|
+
const { text } = await generateText({
|
|
15
|
+
- model: openai("gpt-5.4-mini"),
|
|
16
|
+
+ model: qgrid("openai/gpt-5.4-mini"),
|
|
17
|
+
prompt: "서울 날씨 알려줘",
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
이미 다른 provider(google, openai 등)를 직접 사용하고 있다면, **logger 옵션 한 줄**만 추가하면 에이전트의 매 step(generate, tool-call, reasoning)을 qgrid 대시보드에서 확인할 수 있습니다.
|
|
22
|
+
|
|
23
|
+
```diff
|
|
24
|
+
const { text } = await generateText({
|
|
25
|
+
model: google("gemini-3-flash"),
|
|
26
|
+
prompt: "복잡한 질문",
|
|
27
|
+
+ experimental_telemetry: createQgridLogger({ serverUrl: "http://localhost:44900" }),
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 설치
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pnpm add @cartanova/qgrid-ai-sdk
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Peer dependencies: `ai@^6.0.0`, `@ai-sdk/provider@^3.0.0`
|
|
38
|
+
|
|
39
|
+
## 빠른 시작
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { generateText } from "ai";
|
|
43
|
+
import { qgrid } from "@cartanova/qgrid-ai-sdk";
|
|
44
|
+
|
|
45
|
+
const { text } = await generateText({
|
|
46
|
+
model: qgrid("openai/gpt-5.4-mini"),
|
|
47
|
+
prompt: "서울 날씨 알려줘",
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
qgrid 서버(`http://localhost:44900`)가 실행 중이어야 합니다.
|
|
52
|
+
|
|
53
|
+
## 사용법
|
|
54
|
+
> 들어가기전에: 모든 클라이언트 사용법은 [AI-SDK](https://ai-sdk.dev/docs/ai-sdk-core)와 동일합니다.
|
|
55
|
+
|
|
56
|
+
### 텍스트 생성
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { generateText } from "ai";
|
|
60
|
+
import { qgrid } from "@cartanova/qgrid-ai-sdk";
|
|
61
|
+
|
|
62
|
+
const { text } = await generateText({
|
|
63
|
+
model: qgrid("openai/gpt-5.4-mini"),
|
|
64
|
+
system: "당신은 학술 논문 요약가입니다.",
|
|
65
|
+
prompt: paperText,
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 구조화 응답 (Structured Output)
|
|
70
|
+
> [AI-SDK structured output guide를 참조하세요](https://ai-sdk.dev/docs/ai-sdk-core/generating-structured-data)
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { generateText, Output } from "ai";
|
|
74
|
+
import { qgrid } from "@cartanova/qgrid-ai-sdk";
|
|
75
|
+
import { z } from "zod";
|
|
76
|
+
|
|
77
|
+
const { output } = await generateText({
|
|
78
|
+
model: qgrid("openai/gpt-5.4"),
|
|
79
|
+
system: "논문 메타데이터를 추출해주세요.",
|
|
80
|
+
prompt: paperText,
|
|
81
|
+
output: Output.object({
|
|
82
|
+
schema: z.object({
|
|
83
|
+
title: z.string(),
|
|
84
|
+
authors: z.array(z.string()),
|
|
85
|
+
keyFindings: z.array(z.string()),
|
|
86
|
+
}),
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
console.log(output.title, output.authors);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
top-level이 `object`인 schema는 서버 structured output으로 전달되어 강제됩니다.
|
|
94
|
+
top-level이 `object`가 아니면 (예: array) AI SDK 클라이언트 파싱으로 fallback되며 경고 로그가 출력됩니다.
|
|
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 파싱을 깨뜨릴 수 있어 기본값 유지를 권장합니다.
|
|
97
|
+
|
|
98
|
+
### 스트리밍
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { streamText } from "ai";
|
|
102
|
+
import { qgrid } from "@cartanova/qgrid-ai-sdk";
|
|
103
|
+
|
|
104
|
+
const { textStream } = streamText({
|
|
105
|
+
model: qgrid("openai/gpt-5.4-mini"),
|
|
106
|
+
prompt: "TypeScript의 장점을 설명해줘",
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
for await (const chunk of textStream) {
|
|
110
|
+
process.stdout.write(chunk);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Tool Calling
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import { generateText, tool } from "ai";
|
|
118
|
+
import { qgrid } from "@cartanova/qgrid-ai-sdk";
|
|
119
|
+
import { z } from "zod";
|
|
120
|
+
|
|
121
|
+
const { text } = await generateText({
|
|
122
|
+
model: qgrid("openai/gpt-5.4-mini"),
|
|
123
|
+
prompt: "서울 날씨 알려줘",
|
|
124
|
+
tools: {
|
|
125
|
+
getWeather: tool({
|
|
126
|
+
description: "도시의 현재 날씨 조회",
|
|
127
|
+
parameters: z.object({ city: z.string() }),
|
|
128
|
+
execute: async ({ city }) => {
|
|
129
|
+
return { temperature: 22, condition: "맑음" };
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
tool-call은 qgrid 서버의 structured output emulation으로 동작합니다.
|
|
137
|
+
AI SDK가 tool 실행을 관리하고, qgrid는 각 턴의 LLM 호출만 담당합니다.
|
|
138
|
+
|
|
139
|
+
### Provider Options
|
|
140
|
+
|
|
141
|
+
qgrid 전용 옵션은 전부 `providerOptions.qgrid` 네임스페이스로 전달합니다. (`providerOptions.openai`가 아닙니다)
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { generateText } from "ai";
|
|
145
|
+
import { qgrid, type QgridProviderOptions } from "@cartanova/qgrid-ai-sdk";
|
|
146
|
+
|
|
147
|
+
const { text } = await generateText({
|
|
148
|
+
model: qgrid("openai/gpt-5.4"),
|
|
149
|
+
prompt: "복잡한 문제를 분석해줘",
|
|
150
|
+
providerOptions: {
|
|
151
|
+
qgrid: {
|
|
152
|
+
effort: "high",
|
|
153
|
+
reasoningSummary: "concise",
|
|
154
|
+
verbosity: "medium",
|
|
155
|
+
} satisfies QgridProviderOptions,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
| 옵션 | 값 | 적용 범위 | 설명 |
|
|
161
|
+
|---|---|---|---|
|
|
162
|
+
| `sessionKey` | `string` | OpenAI 전용 | 멀티턴 대화 식별자. 같은 key는 같은 codex thread로 라우팅되어 prompt cache 적중 ([아래](#멀티턴-prompt-cache-sessionkey) 참조) |
|
|
163
|
+
| `effort` | `"none"` \| `"minimal"` \| `"low"` \| `"medium"` \| `"high"` \| `"xhigh"` | 공통 | reasoning 모델의 추론 깊이. 기본값은 config의 `defaultEffort` (`"low"`) |
|
|
164
|
+
| `verbosity` | `"low"` \| `"medium"` \| `"high"` | OpenAI 전용 | 응답 텍스트의 상세도 |
|
|
165
|
+
| `reasoningSummary` | `"auto"` \| `"concise"` \| `"detailed"` \| `"none"` | OpenAI 전용 | 추론 요약 출력 방식 |
|
|
166
|
+
| `serviceTier` | `string` | OpenAI 전용 | OpenAI/codex service tier |
|
|
167
|
+
| `imageGeneration` | `boolean` | OpenAI 전용, non-stream | codex 내장 `image_generation` tool 활성화 ([아래](#image-generation) 참조) |
|
|
168
|
+
| `imageGenerationOptions` | `{ quality?, size? }` | OpenAI 전용 | 이미지 품질/크기 힌트. `quality: "low" \| "medium" \| "high"`, `size: "1024x1024" \| "1024x1536" \| "1536x1024"` (기본: `medium` / `1536x1024`) |
|
|
169
|
+
| `fallbackModels` | `string[]` | 예약 | 향후 서버 fallback routing용 예약 필드. 현재 동작하지 않음 |
|
|
170
|
+
|
|
171
|
+
### 멀티턴 prompt cache (sessionKey)
|
|
172
|
+
|
|
173
|
+
멀티턴 대화에서 `sessionKey`로 호출자의 도메인 ID(게임 세션 ID, 채팅방 ID 등) 하나만 넘기면, provider가 같은 대화를 같은 codex thread로 라우팅하여 prompt cache를 적중시킵니다. thread 좌표의 발급/보관/회송은 provider 내부에서 처리하므로 호출자는 key만 넘기면 됩니다.
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
const { text } = await generateText({
|
|
177
|
+
model: qgrid("openai/gpt-5.4-mini"),
|
|
178
|
+
prompt: nextTurnPrompt,
|
|
179
|
+
providerOptions: { qgrid: { sessionKey: "game-session-123" } },
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
- thread는 idle 10분 후 만료됩니다. 만료되거나 worker가 재시작되면 자동으로 cold thread로 시작하므로 요청이 실패하지는 않습니다.
|
|
184
|
+
- `anthropic/*` 모델에서는 무시됩니다 (Claude Code는 요청마다 fresh process를 띄우므로 thread reuse가 없음).
|
|
185
|
+
- 서버에서 `QGRID_OPENAI_THREAD_REUSE=false`로 비활성화할 수 있습니다.
|
|
186
|
+
|
|
187
|
+
### Image Generation
|
|
188
|
+
|
|
189
|
+
OpenAI/codex 경로 전용, `generateText` 전용입니다. 해당 요청에만 codex 내장 `image_generation` tool을 켜고, 결과 이미지를 AI SDK `files`로 받습니다.
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
const result = await generateText({
|
|
193
|
+
model: qgrid("openai/gpt-5.4"),
|
|
194
|
+
prompt: "우주를 나는 고래 일러스트",
|
|
195
|
+
providerOptions: {
|
|
196
|
+
qgrid: {
|
|
197
|
+
imageGeneration: true,
|
|
198
|
+
imageGenerationOptions: { quality: "medium", size: "1536x1024" },
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const image = result.files[0]; // mediaType: "image/png", base64
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
- `streamText`에서는 거부됩니다 (non-stream 전용).
|
|
207
|
+
- 이미지 생성 요청은 항상 cold one-shot thread로 실행되며 thread reuse 대상에서 제외됩니다.
|
|
208
|
+
- 이미지 비용은 `gpt-image-2` 공개 단가표 기반 **추정치**로 request log의 `image_cost_usd`에 별도 기록됩니다 (codex가 정확한 이미지 tool 사용량을 노출하지 않음).
|
|
209
|
+
|
|
210
|
+
## Telemetry Logger
|
|
211
|
+
|
|
212
|
+
qgrid provider가 아닌 모델(google, openai 직접 호출)에서도 같은 request log 대시보드를 사용하려면 `createQgridLogger`를 `experimental_telemetry`에 넣으면 됩니다.
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import { generateText } from "ai";
|
|
216
|
+
import { google } from "@ai-sdk/google";
|
|
217
|
+
import { createQgridLogger } from "@cartanova/qgrid-ai-sdk";
|
|
218
|
+
|
|
219
|
+
const { text } = await generateText({
|
|
220
|
+
model: google("gemini-3-flash"),
|
|
221
|
+
prompt: "안녕하세요",
|
|
222
|
+
experimental_telemetry: createQgridLogger({
|
|
223
|
+
serverUrl: "http://localhost:44900",
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
병렬 호출, run 분리, telemetry 활성화 등은 자동 처리됩니다.
|
|
229
|
+
|
|
230
|
+
### Logger 설정
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
createQgridLogger({
|
|
234
|
+
serverUrl: string; // qgrid 서버 주소 (필수)
|
|
235
|
+
projectName?: string; // request_logs.project_name (기본: QGRID_PROJECT_NAME 환경변수)
|
|
236
|
+
tokenName?: string; // request_logs.token_name (기본: "external")
|
|
237
|
+
staleRunTimeoutMs?: number; // watchdog timeout (기본: 30분 또는 AI SDK timeout + 여유시간, 0으로 비활성화)
|
|
238
|
+
onLogError?: (error: Error) => void; // 로깅 실패 콜백
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
모든 설정은 optional (serverUrl 제외). 기본값이 있으므로 `serverUrl`만 넣으면 동작합니다.
|
|
243
|
+
|
|
244
|
+
### qgrid provider와 함께 사용
|
|
245
|
+
|
|
246
|
+
`qgrid()` provider는 자체 lifecycle이 있으므로 logger가 자동으로 suppress됩니다. 같은 코드에서 qgrid provider와 다른 provider를 섞어 써도 이중 기록되지 않습니다.
|
|
247
|
+
|
|
248
|
+
## 지원 모델
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
type QgridSupportedModel =
|
|
252
|
+
// OpenAI (based on codex app-server)
|
|
253
|
+
| "openai/gpt-5.5"
|
|
254
|
+
| "openai/gpt-5.4"
|
|
255
|
+
| "openai/gpt-5.2"
|
|
256
|
+
| "openai/gpt-5.4-mini"
|
|
257
|
+
| "openai/gpt-5.3-codex"
|
|
258
|
+
| "openai/gpt-5.3-codex-spark"
|
|
259
|
+
// Anthropic
|
|
260
|
+
| "anthropic/claude-haiku-4-5"
|
|
261
|
+
| "anthropic/claude-sonnet-4"
|
|
262
|
+
| "anthropic/claude-sonnet-4-5"
|
|
263
|
+
| "anthropic/claude-sonnet-4-6"
|
|
264
|
+
| "anthropic/claude-sonnet-4-7"
|
|
265
|
+
| "anthropic/claude-sonnet-5"
|
|
266
|
+
| "anthropic/claude-opus-4"
|
|
267
|
+
| "anthropic/claude-opus-4-1"
|
|
268
|
+
| "anthropic/claude-opus-4-5"
|
|
269
|
+
| "anthropic/claude-opus-4-6"
|
|
270
|
+
| "anthropic/claude-opus-4-7"
|
|
271
|
+
| "anthropic/claude-opus-4-8"
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## 설정
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
qgrid(modelId, {
|
|
278
|
+
serverUrl?: string; // qgrid 서버 주소 (기본: QGRID_URL 환경변수 또는 http://localhost:44900)
|
|
279
|
+
defaultEffort?: string; // effort 기본값 (기본: "low")
|
|
280
|
+
projectName?: string; // request_logs.project_name (기본: QGRID_PROJECT_NAME 환경변수)
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
여러 프로젝트/워크플로우가 한 qgrid 서버를 공유한다면 `QGRID_PROJECT_NAME`을 설정하세요. 대시보드에서 request log를 프로젝트별로 필터링하고 토큰/비용/캐시 지표를 워크로드별로 비교할 수 있습니다. config `projectName`은 특정 호출자만 다른 이름을 써야 할 때의 override 용도입니다.
|
|
285
|
+
|
|
286
|
+
## 환경변수
|
|
287
|
+
|
|
288
|
+
| 변수 | 설명 | 기본값 |
|
|
289
|
+
|---|---|---|
|
|
290
|
+
| `QGRID_URL` | qgrid 서버 주소 | `http://localhost:44900` |
|
|
291
|
+
| `QGRID_PROJECT_NAME` | request log 프로젝트 이름 (provider/logger 공통) | (없음) |
|
|
292
|
+
|
|
293
|
+
## 주의사항
|
|
294
|
+
|
|
295
|
+
- `temperature`, `maxOutputTokens` 등 sampling 파라미터는 CLI 런타임(OpenAI: codex app-server, Anthropic: Claude Code)이 지원하지 않아 무시됩니다.
|
|
296
|
+
- Structured output은 top-level `object` schema만 서버에서 강제됩니다. top-level `array`는 클라이언트 파싱 fallback.
|
|
297
|
+
- `tools`와 structured output schema는 서버에서 동시에 사용할 수 없습니다. tools가 있으면 schema는 서버로 전달되지 않고 최종 텍스트를 AI SDK가 클라이언트에서 파싱합니다.
|
|
298
|
+
|
|
299
|
+
## 요구사항
|
|
300
|
+
|
|
301
|
+
- Node.js >= 20
|
|
302
|
+
- AI SDK v6 (`ai@^6.0.0`)
|
|
303
|
+
- 실행 중인 qgrid 서버
|
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# @cartanova/qgrid-ai-sdk
|
|
2
2
|
|
|
3
|
+
**English** · [한국어](./README.ko.md)
|
|
4
|
+
|
|
3
5
|
AI SDK v6 custom `LanguageModelV3` provider for [qgrid](https://github.com/cartanova-ai/Qgrid).
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
**Without changing your existing AI SDK code, swap a single `model` line to get subscription-token pooling (N tokens × parallel workers) + the request log dashboard.**
|
|
6
8
|
|
|
7
9
|
```diff
|
|
8
10
|
import { generateText } from "ai";
|
|
@@ -12,21 +14,21 @@ AI SDK v6 custom `LanguageModelV3` provider for [qgrid](https://github.com/carta
|
|
|
12
14
|
const { text } = await generateText({
|
|
13
15
|
- model: openai("gpt-5.4-mini"),
|
|
14
16
|
+ model: qgrid("openai/gpt-5.4-mini"),
|
|
15
|
-
prompt: "
|
|
17
|
+
prompt: "What's the weather in Seoul?",
|
|
16
18
|
});
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
If you already use another provider (google, openai, ...) directly, add **a single logger line** to see every agent step (generate, tool-call, reasoning) in the qgrid dashboard.
|
|
20
22
|
|
|
21
23
|
```diff
|
|
22
24
|
const { text } = await generateText({
|
|
23
25
|
model: google("gemini-3-flash"),
|
|
24
|
-
prompt: "
|
|
26
|
+
prompt: "A complex question",
|
|
25
27
|
+ experimental_telemetry: createQgridLogger({ serverUrl: "http://localhost:44900" }),
|
|
26
28
|
});
|
|
27
29
|
```
|
|
28
30
|
|
|
29
|
-
##
|
|
31
|
+
## Install
|
|
30
32
|
|
|
31
33
|
```bash
|
|
32
34
|
pnpm add @cartanova/qgrid-ai-sdk
|
|
@@ -34,7 +36,7 @@ pnpm add @cartanova/qgrid-ai-sdk
|
|
|
34
36
|
|
|
35
37
|
Peer dependencies: `ai@^6.0.0`, `@ai-sdk/provider@^3.0.0`
|
|
36
38
|
|
|
37
|
-
##
|
|
39
|
+
## Quick start
|
|
38
40
|
|
|
39
41
|
```typescript
|
|
40
42
|
import { generateText } from "ai";
|
|
@@ -42,16 +44,16 @@ import { qgrid } from "@cartanova/qgrid-ai-sdk";
|
|
|
42
44
|
|
|
43
45
|
const { text } = await generateText({
|
|
44
46
|
model: qgrid("openai/gpt-5.4-mini"),
|
|
45
|
-
prompt: "
|
|
47
|
+
prompt: "What's the weather in Seoul?",
|
|
46
48
|
});
|
|
47
49
|
```
|
|
48
50
|
|
|
49
|
-
qgrid
|
|
51
|
+
A qgrid server (`http://localhost:44900`) must be running.
|
|
50
52
|
|
|
51
|
-
##
|
|
52
|
-
>
|
|
53
|
+
## Usage
|
|
54
|
+
> Before you start: all client-side usage is identical to the [AI SDK](https://ai-sdk.dev/docs/ai-sdk-core).
|
|
53
55
|
|
|
54
|
-
###
|
|
56
|
+
### Text generation
|
|
55
57
|
|
|
56
58
|
```typescript
|
|
57
59
|
import { generateText } from "ai";
|
|
@@ -59,13 +61,13 @@ import { qgrid } from "@cartanova/qgrid-ai-sdk";
|
|
|
59
61
|
|
|
60
62
|
const { text } = await generateText({
|
|
61
63
|
model: qgrid("openai/gpt-5.4-mini"),
|
|
62
|
-
system: "
|
|
64
|
+
system: "You are an academic paper summarizer.",
|
|
63
65
|
prompt: paperText,
|
|
64
66
|
});
|
|
65
67
|
```
|
|
66
68
|
|
|
67
|
-
###
|
|
68
|
-
> [AI
|
|
69
|
+
### Structured Output
|
|
70
|
+
> See the [AI SDK structured output guide](https://ai-sdk.dev/docs/ai-sdk-core/generating-structured-data)
|
|
69
71
|
|
|
70
72
|
```typescript
|
|
71
73
|
import { generateText, Output } from "ai";
|
|
@@ -74,7 +76,7 @@ import { z } from "zod";
|
|
|
74
76
|
|
|
75
77
|
const { output } = await generateText({
|
|
76
78
|
model: qgrid("openai/gpt-5.4"),
|
|
77
|
-
system: "
|
|
79
|
+
system: "Extract the paper metadata.",
|
|
78
80
|
prompt: paperText,
|
|
79
81
|
output: Output.object({
|
|
80
82
|
schema: z.object({
|
|
@@ -88,10 +90,12 @@ const { output } = await generateText({
|
|
|
88
90
|
console.log(output.title, output.authors);
|
|
89
91
|
```
|
|
90
92
|
|
|
91
|
-
top-level
|
|
92
|
-
top
|
|
93
|
+
Schemas with a top-level `object` are forwarded to server-side structured output and enforced.
|
|
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
|
+
|
|
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.
|
|
93
97
|
|
|
94
|
-
###
|
|
98
|
+
### Streaming
|
|
95
99
|
|
|
96
100
|
```typescript
|
|
97
101
|
import { streamText } from "ai";
|
|
@@ -99,7 +103,7 @@ import { qgrid } from "@cartanova/qgrid-ai-sdk";
|
|
|
99
103
|
|
|
100
104
|
const { textStream } = streamText({
|
|
101
105
|
model: qgrid("openai/gpt-5.4-mini"),
|
|
102
|
-
prompt: "
|
|
106
|
+
prompt: "Explain the benefits of TypeScript",
|
|
103
107
|
});
|
|
104
108
|
|
|
105
109
|
for await (const chunk of textStream) {
|
|
@@ -116,54 +120,96 @@ import { z } from "zod";
|
|
|
116
120
|
|
|
117
121
|
const { text } = await generateText({
|
|
118
122
|
model: qgrid("openai/gpt-5.4-mini"),
|
|
119
|
-
prompt: "
|
|
123
|
+
prompt: "What's the weather in Seoul?",
|
|
120
124
|
tools: {
|
|
121
125
|
getWeather: tool({
|
|
122
|
-
description: "
|
|
126
|
+
description: "Get the current weather for a city",
|
|
123
127
|
parameters: z.object({ city: z.string() }),
|
|
124
128
|
execute: async ({ city }) => {
|
|
125
|
-
return { temperature: 22, condition: "
|
|
129
|
+
return { temperature: 22, condition: "sunny" };
|
|
126
130
|
},
|
|
127
131
|
}),
|
|
128
132
|
},
|
|
129
133
|
});
|
|
130
134
|
```
|
|
131
135
|
|
|
132
|
-
|
|
133
|
-
AI SDK
|
|
136
|
+
Tool calls work through the qgrid server's structured output emulation.
|
|
137
|
+
The AI SDK manages tool execution; qgrid handles only each turn's LLM call.
|
|
134
138
|
|
|
135
139
|
### Provider Options
|
|
136
140
|
|
|
141
|
+
All qgrid-specific options go under the `providerOptions.qgrid` namespace. (Not `providerOptions.openai`.)
|
|
142
|
+
|
|
137
143
|
```typescript
|
|
138
144
|
import { generateText } from "ai";
|
|
139
145
|
import { qgrid, type QgridProviderOptions } from "@cartanova/qgrid-ai-sdk";
|
|
140
146
|
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
const { text } = await generateText({
|
|
148
|
+
model: qgrid("openai/gpt-5.4"),
|
|
149
|
+
prompt: "Analyze this complex problem",
|
|
150
|
+
providerOptions: {
|
|
151
|
+
qgrid: {
|
|
152
|
+
effort: "high",
|
|
153
|
+
reasoningSummary: "concise",
|
|
154
|
+
verbosity: "medium",
|
|
155
|
+
} satisfies QgridProviderOptions,
|
|
146
156
|
},
|
|
147
|
-
}
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
| Option | Values | Applies to | Description |
|
|
161
|
+
|---|---|---|---|
|
|
162
|
+
| `sessionKey` | `string` | OpenAI only | Multi-turn conversation identifier. The same key routes to the same codex thread for prompt-cache hits (see [below](#multi-turn-prompt-cache-sessionkey)) |
|
|
163
|
+
| `effort` | `"none"` \| `"minimal"` \| `"low"` \| `"medium"` \| `"high"` \| `"xhigh"` | both providers | Reasoning depth. Defaults to the config's `defaultEffort` (`"low"`) |
|
|
164
|
+
| `verbosity` | `"low"` \| `"medium"` \| `"high"` | OpenAI only | Response text verbosity |
|
|
165
|
+
| `reasoningSummary` | `"auto"` \| `"concise"` \| `"detailed"` \| `"none"` | OpenAI only | Reasoning summary output mode |
|
|
166
|
+
| `serviceTier` | `string` | OpenAI only | OpenAI/codex service tier |
|
|
167
|
+
| `imageGeneration` | `boolean` | OpenAI only, non-stream | Enables codex's built-in `image_generation` tool (see [below](#image-generation)) |
|
|
168
|
+
| `imageGenerationOptions` | `{ quality?, size? }` | OpenAI only | Image quality/size hints. `quality: "low" \| "medium" \| "high"`, `size: "1024x1024" \| "1024x1536" \| "1536x1024"` (defaults: `medium` / `1536x1024`) |
|
|
169
|
+
| `fallbackModels` | `string[]` | reserved | Reserved for future server-side fallback routing. Not functional yet |
|
|
170
|
+
|
|
171
|
+
### Multi-turn prompt cache (sessionKey)
|
|
148
172
|
|
|
173
|
+
For multi-turn conversations, pass a single caller-side domain ID (game session ID, chat room ID, ...) as `sessionKey` and the provider routes the same conversation to the same codex thread, hitting the prompt cache. Thread coordinate issuance/storage/replay is handled inside the provider — the caller only passes the key.
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
149
176
|
const { text } = await generateText({
|
|
150
|
-
model: qgrid("openai/gpt-5.4"),
|
|
151
|
-
prompt:
|
|
152
|
-
providerOptions,
|
|
177
|
+
model: qgrid("openai/gpt-5.4-mini"),
|
|
178
|
+
prompt: nextTurnPrompt,
|
|
179
|
+
providerOptions: { qgrid: { sessionKey: "game-session-123" } },
|
|
153
180
|
});
|
|
154
181
|
```
|
|
155
182
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
183
|
+
- Threads expire after 10 minutes idle. On expiry or worker restart, the request automatically starts a cold thread instead of failing.
|
|
184
|
+
- Ignored for `anthropic/*` models (Claude Code spawns a fresh process per request, so there is no thread reuse).
|
|
185
|
+
- Can be disabled server-side with `QGRID_OPENAI_THREAD_REUSE=false`.
|
|
186
|
+
|
|
187
|
+
### Image Generation
|
|
188
|
+
|
|
189
|
+
OpenAI/codex route only, `generateText` only. Enables codex's built-in `image_generation` tool for that single request and returns the image through the AI SDK `files`.
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
const result = await generateText({
|
|
193
|
+
model: qgrid("openai/gpt-5.4"),
|
|
194
|
+
prompt: "An illustration of a whale flying through space",
|
|
195
|
+
providerOptions: {
|
|
196
|
+
qgrid: {
|
|
197
|
+
imageGeneration: true,
|
|
198
|
+
imageGenerationOptions: { quality: "medium", size: "1536x1024" },
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
});
|
|
161
202
|
|
|
162
|
-
|
|
203
|
+
const image = result.files[0]; // mediaType: "image/png", base64
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
- Rejected in `streamText` (non-stream only).
|
|
207
|
+
- Image-generation requests always run on a cold one-shot thread and are excluded from thread reuse.
|
|
208
|
+
- The image cost is an **estimate** based on the public `gpt-image-2` price table, recorded separately as `image_cost_usd` on the request log (codex does not expose exact image-tool usage).
|
|
163
209
|
|
|
164
210
|
## Telemetry Logger
|
|
165
211
|
|
|
166
|
-
|
|
212
|
+
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`.
|
|
167
213
|
|
|
168
214
|
```typescript
|
|
169
215
|
import { generateText } from "ai";
|
|
@@ -172,34 +218,34 @@ import { createQgridLogger } from "@cartanova/qgrid-ai-sdk";
|
|
|
172
218
|
|
|
173
219
|
const { text } = await generateText({
|
|
174
220
|
model: google("gemini-3-flash"),
|
|
175
|
-
prompt: "
|
|
221
|
+
prompt: "Hello",
|
|
176
222
|
experimental_telemetry: createQgridLogger({
|
|
177
223
|
serverUrl: "http://localhost:44900",
|
|
178
224
|
}),
|
|
179
225
|
});
|
|
180
226
|
```
|
|
181
227
|
|
|
182
|
-
|
|
228
|
+
Parallel calls, run separation, and telemetry activation are handled automatically.
|
|
183
229
|
|
|
184
|
-
### Logger
|
|
230
|
+
### Logger configuration
|
|
185
231
|
|
|
186
232
|
```typescript
|
|
187
233
|
createQgridLogger({
|
|
188
|
-
serverUrl: string; // qgrid
|
|
189
|
-
projectName?: string; // request_logs.project_name
|
|
190
|
-
tokenName?: string; // request_logs.token_name (
|
|
191
|
-
staleRunTimeoutMs?: number; // watchdog timeout (
|
|
192
|
-
onLogError?: (error: Error) => void; //
|
|
234
|
+
serverUrl: string; // qgrid server address (required)
|
|
235
|
+
projectName?: string; // request_logs.project_name (default: QGRID_PROJECT_NAME env var)
|
|
236
|
+
tokenName?: string; // request_logs.token_name (default: "external")
|
|
237
|
+
staleRunTimeoutMs?: number; // watchdog timeout (default: 30 min, or the AI SDK timeout plus a grace period; 0 disables)
|
|
238
|
+
onLogError?: (error: Error) => void; // logging-failure callback
|
|
193
239
|
});
|
|
194
240
|
```
|
|
195
241
|
|
|
196
|
-
|
|
242
|
+
Everything except `serverUrl` is optional. With defaults in place, passing only `serverUrl` works.
|
|
197
243
|
|
|
198
|
-
### qgrid provider
|
|
244
|
+
### Using alongside the qgrid provider
|
|
199
245
|
|
|
200
|
-
`qgrid()` provider
|
|
246
|
+
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.
|
|
201
247
|
|
|
202
|
-
##
|
|
248
|
+
## Supported models
|
|
203
249
|
|
|
204
250
|
```typescript
|
|
205
251
|
type QgridSupportedModel =
|
|
@@ -216,6 +262,7 @@ type QgridSupportedModel =
|
|
|
216
262
|
| "anthropic/claude-sonnet-4-5"
|
|
217
263
|
| "anthropic/claude-sonnet-4-6"
|
|
218
264
|
| "anthropic/claude-sonnet-4-7"
|
|
265
|
+
| "anthropic/claude-sonnet-5"
|
|
219
266
|
| "anthropic/claude-opus-4"
|
|
220
267
|
| "anthropic/claude-opus-4-1"
|
|
221
268
|
| "anthropic/claude-opus-4-5"
|
|
@@ -224,28 +271,33 @@ type QgridSupportedModel =
|
|
|
224
271
|
| "anthropic/claude-opus-4-8"
|
|
225
272
|
```
|
|
226
273
|
|
|
227
|
-
##
|
|
274
|
+
## Configuration
|
|
228
275
|
|
|
229
276
|
```typescript
|
|
230
277
|
qgrid(modelId, {
|
|
231
|
-
serverUrl?: string; // qgrid
|
|
232
|
-
defaultEffort?: string; //
|
|
278
|
+
serverUrl?: string; // qgrid server address (default: QGRID_URL env var, then http://localhost:44900)
|
|
279
|
+
defaultEffort?: string; // default effort (default: "low")
|
|
280
|
+
projectName?: string; // request_logs.project_name (default: QGRID_PROJECT_NAME env var)
|
|
233
281
|
});
|
|
234
282
|
```
|
|
235
283
|
|
|
236
|
-
|
|
284
|
+
If multiple projects/workflows share one qgrid server, set `QGRID_PROJECT_NAME`. It lets you filter request logs by project in the dashboard and compare token/cost/cache metrics per workload. The config-level `projectName` is an override for callers that need a different label.
|
|
285
|
+
|
|
286
|
+
## Environment variables
|
|
237
287
|
|
|
238
|
-
|
|
|
288
|
+
| Variable | Description | Default |
|
|
239
289
|
|---|---|---|
|
|
240
|
-
| `QGRID_URL` | qgrid
|
|
290
|
+
| `QGRID_URL` | qgrid server address | `http://localhost:44900` |
|
|
291
|
+
| `QGRID_PROJECT_NAME` | request log project name (provider and logger) | (empty) |
|
|
241
292
|
|
|
242
|
-
##
|
|
293
|
+
## Notes
|
|
243
294
|
|
|
244
|
-
- `temperature
|
|
245
|
-
- Structured output
|
|
295
|
+
- Sampling parameters such as `temperature` and `maxOutputTokens` are ignored — the CLI runtimes (OpenAI: codex app-server, Anthropic: Claude Code) do not support them.
|
|
296
|
+
- Structured output is server-enforced only for top-level `object` schemas. Top-level `array` falls back to client-side parsing.
|
|
297
|
+
- `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.
|
|
246
298
|
|
|
247
|
-
##
|
|
299
|
+
## Requirements
|
|
248
300
|
|
|
249
301
|
- Node.js >= 20
|
|
250
302
|
- AI SDK v6 (`ai@^6.0.0`)
|
|
251
|
-
-
|
|
303
|
+
- A running qgrid server
|
package/dist/index.d.ts
CHANGED
|
@@ -29,6 +29,20 @@ type QgridProviderOptions = {
|
|
|
29
29
|
serviceTier?: string;
|
|
30
30
|
/** @todo 향후 qgrid 서버 fallback routing에 사용할 후보 모델 목록. */
|
|
31
31
|
fallbackModels?: string[];
|
|
32
|
+
/**
|
|
33
|
+
* codex 내장 image_generation tool 을 켠다. OpenAI/codex route + non-stream 전용.
|
|
34
|
+
* generateText 결과의 files 로 이미지를 받는다. streaming(streamText)에서는 거부된다.
|
|
35
|
+
*/
|
|
36
|
+
imageGeneration?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Image generation hint and cost-estimation basis. The image model is fixed
|
|
39
|
+
* to gpt-image-2; qgrid accepts supported quality/size pairs from OpenAI's
|
|
40
|
+
* public calculator table.
|
|
41
|
+
*/
|
|
42
|
+
imageGenerationOptions?: {
|
|
43
|
+
quality?: "low" | "medium" | "high";
|
|
44
|
+
size?: "1024x1024" | "1024x1536" | "1536x1024";
|
|
45
|
+
};
|
|
32
46
|
};
|
|
33
47
|
/**
|
|
34
48
|
* codex thread 좌표
|
package/dist/index.js
CHANGED
|
@@ -528,6 +528,8 @@ function qgrid(modelId, config) {
|
|
|
528
528
|
const verbosity = qgridOptions.verbosity;
|
|
529
529
|
const reasoningSummary = qgridOptions.reasoningSummary;
|
|
530
530
|
const serviceTier = qgridOptions.serviceTier;
|
|
531
|
+
const imageGeneration = qgridOptions.imageGeneration;
|
|
532
|
+
const imageGenerationOptions = qgridOptions.imageGenerationOptions;
|
|
531
533
|
const sessionKey = qgridOptions.sessionKey;
|
|
532
534
|
const reuseSessionKey = reusableSessionKey(modelId, sessionKey);
|
|
533
535
|
const rawSchema = options.responseFormat?.type === "json" ? options.responseFormat.schema : void 0;
|
|
@@ -572,7 +574,9 @@ function qgrid(modelId, config) {
|
|
|
572
574
|
...projectName ? { projectName } : {},
|
|
573
575
|
...logMode ? { logMode } : {},
|
|
574
576
|
...runContext ? { runContext } : {},
|
|
575
|
-
...toolResultsPayload ? { toolResults: toolResultsPayload } : {}
|
|
577
|
+
...toolResultsPayload ? { toolResults: toolResultsPayload } : {},
|
|
578
|
+
...imageGeneration ? { imageGeneration } : {},
|
|
579
|
+
...imageGenerationOptions ? { imageGenerationOptions } : {}
|
|
576
580
|
} }),
|
|
577
581
|
signal: options.abortSignal
|
|
578
582
|
}).then(async (res) => {
|
|
@@ -592,7 +596,12 @@ function qgrid(modelId, config) {
|
|
|
592
596
|
type: "text",
|
|
593
597
|
text: item.text
|
|
594
598
|
});
|
|
595
|
-
else content.push({
|
|
599
|
+
else if (item.type === "image") content.push({
|
|
600
|
+
type: "file",
|
|
601
|
+
mediaType: "image/png",
|
|
602
|
+
data: item.data
|
|
603
|
+
});
|
|
604
|
+
else if (item.type === "tool-call") content.push({
|
|
596
605
|
type: "tool-call",
|
|
597
606
|
toolCallId: item.toolCallId,
|
|
598
607
|
toolName: item.toolName,
|
|
@@ -606,6 +615,7 @@ function qgrid(modelId, config) {
|
|
|
606
615
|
type: "text",
|
|
607
616
|
text: data.text
|
|
608
617
|
});
|
|
618
|
+
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.");
|
|
609
619
|
if (data.runContext && finishReason.unified === "tool-calls") clientRun = {
|
|
610
620
|
runContext: data.runContext,
|
|
611
621
|
pendingToolCallIds: new Set(content.filter((c) => c.type === "tool-call").map((c) => c.toolCallId))
|
|
@@ -635,6 +645,7 @@ function qgrid(modelId, config) {
|
|
|
635
645
|
const verbosity = qgridOptions.verbosity;
|
|
636
646
|
const reasoningSummary = qgridOptions.reasoningSummary;
|
|
637
647
|
const serviceTier = qgridOptions.serviceTier;
|
|
648
|
+
if (qgridOptions.imageGeneration) throw new Error("qgrid: imageGeneration is not supported with streamText — use generateText instead.");
|
|
638
649
|
const sessionKey = qgridOptions.sessionKey;
|
|
639
650
|
const reuseSessionKey = reusableSessionKey(modelId, sessionKey);
|
|
640
651
|
const rawSchema = options.responseFormat?.type === "json" ? options.responseFormat.schema : void 0;
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cartanova/qgrid-ai-sdk",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.9",
|
|
4
4
|
"description": "AI SDK LanguageModelV3 provider for qgrid",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/cartanova-ai/qgrid"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"dist"
|
|
10
|
+
"dist",
|
|
11
|
+
"README.ko.md"
|
|
11
12
|
],
|
|
12
13
|
"type": "module",
|
|
13
14
|
"main": "dist/index.js",
|