@cartanova/qgrid-ai-sdk 2.7.0 → 2.7.2
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 +18 -12
- package/README.md +29 -16
- package/dist/index.d.ts +10 -9
- package/dist/index.js +111 -16
- package/package.json +4 -1
package/README.ko.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
AI SDK v6 custom `LanguageModelV3` provider for [qgrid](https://github.com/cartanova-ai/Qgrid).
|
|
6
6
|
|
|
7
|
-
**기존 AI SDK 코드 변경 없이, `model` 한 줄만 바꾸면 구독 토큰 풀링(토큰 N개 ×
|
|
7
|
+
**기존 AI SDK 코드 변경 없이, `model` 한 줄만 바꾸면 구독 토큰 풀링(토큰 N개 × concurrent permit) + request log 대시보드를 사용할 수 있습니다.**
|
|
8
8
|
|
|
9
9
|
```diff
|
|
10
10
|
import { generateText } from "ai";
|
|
@@ -50,6 +50,8 @@ const { text } = await generateText({
|
|
|
50
50
|
|
|
51
51
|
qgrid 서버(`http://localhost:44900`)가 실행 중이어야 합니다.
|
|
52
52
|
|
|
53
|
+
OpenAI 서버 경로는 private ChatGPT Codex Responses backend를 HTTPS/SSE로 직접 호출합니다. 이 backend는 문서화되지 않아 예고 없이 변경될 수 있으며, qgrid의 mock protocol test는 live provider 검증이 아닙니다.
|
|
54
|
+
|
|
53
55
|
## 사용법
|
|
54
56
|
> 들어가기전에: 모든 클라이언트 사용법은 [AI-SDK](https://ai-sdk.dev/docs/ai-sdk-core)와 동일합니다.
|
|
55
57
|
|
|
@@ -200,12 +202,12 @@ const { text } = await generateText({
|
|
|
200
202
|
| 옵션 | 값 | 적용 범위 | 설명 |
|
|
201
203
|
|---|---|---|---|
|
|
202
204
|
| `logger` | `boolean` | 공통 | qgrid request log 저장 여부. 기본값은 `true`. `false`로 설정해도 client tool 실행과 multi-step 연결은 계속 동작 |
|
|
203
|
-
| `sessionKey` | `string` | OpenAI 전용 |
|
|
205
|
+
| `sessionKey` | `string` | OpenAI 전용 | 전체 history 재전송 시 불투명 prompt-cache affinity를 파생하는 멀티턴 대화 식별자 ([아래](#멀티턴-prompt-cache-sessionkey) 참조) |
|
|
204
206
|
| `effort` | `"none"` \| `"minimal"` \| `"low"` \| `"medium"` \| `"high"` \| `"xhigh"` \| `"max"` | 공통 (지원 값은 모델별 상이, 예: `"max"`는 GPT-5.6+) | reasoning 모델의 추론 깊이. 기본값은 config의 `defaultEffort` (`"low"`) |
|
|
205
207
|
| `verbosity` | `"low"` \| `"medium"` \| `"high"` | OpenAI 전용 | 응답 텍스트의 상세도 |
|
|
206
208
|
| `reasoningSummary` | `"auto"` \| `"concise"` \| `"detailed"` \| `"none"` | OpenAI 전용 | 추론 요약 출력 방식 |
|
|
207
209
|
| `serviceTier` | `string` | OpenAI 전용 | OpenAI/codex service tier |
|
|
208
|
-
| `timeoutMs` | 양의 정수, 최대 `1_800_000` | Anthropic 전용 | 서버의 Claude Code 프로세스 제한시간(ms). 기본값은 240초 |
|
|
210
|
+
| `timeoutMs` | 양의 정수, 최대 `1_800_000` | Anthropic 전용 | 서버의 Claude Code 프로세스 제한시간(ms). SDK의 non-stream HTTP 제한은 이 값보다 60초 길게 설정. 기본값은 240초 |
|
|
209
211
|
| `imageGeneration` | `boolean` | OpenAI 전용, non-stream | codex 내장 `image_generation` tool 활성화 ([아래](#image-generation) 참조) |
|
|
210
212
|
| `imageGenerationOptions` | `{ quality?, size? }` | OpenAI 전용 | 이미지 품질/크기 힌트. `quality: "low" \| "medium" \| "high"`, `size: "1024x1024" \| "1024x1536" \| "1536x1024"` (기본: `medium` / `1536x1024`) |
|
|
211
213
|
| `fallbackModels` | `string[]` | 예약 | 향후 qgrid 서버 fallback routing용 예약 필드. 현재 동작하지 않으며 Claude Code의 Fable refusal fallback과 무관 |
|
|
@@ -213,11 +215,15 @@ const { text } = await generateText({
|
|
|
213
215
|
AI SDK 최상위 `timeout`은 전체 클라이언트 제한시간이며 custom provider 실행 전에
|
|
214
216
|
`AbortSignal`로 변환됩니다. 따라서 그 숫자 자체는 qgrid에 전달되지 않습니다. qgrid 서버의
|
|
215
217
|
Claude Code 프로세스 제한시간을 바꾸려면 `providerOptions.qgrid.timeoutMs`를 사용하세요.
|
|
216
|
-
|
|
218
|
+
Anthropic `generateText` 요청에는 전역 설정을 바꾸지 않는 요청별 Undici dispatcher가 붙고,
|
|
219
|
+
`headersTimeout`과 `bodyTimeout`은 `timeoutMs + 60_000`으로 설정됩니다. 예를 들어 서버 제한이
|
|
220
|
+
600초면 HTTP 전송 예산은 660초입니다. 이 여유 시간 덕분에 클라이언트 전송 계층보다 서버의
|
|
221
|
+
명시적 timeout 응답이 먼저 도착할 수 있습니다. 클라이언트 취소 또는 비스트리밍 HTTP 연결
|
|
222
|
+
종료도 서버의 provider 실행을 중단합니다.
|
|
217
223
|
|
|
218
224
|
### 멀티턴 prompt cache (sessionKey)
|
|
219
225
|
|
|
220
|
-
멀티턴 대화에서 `sessionKey`로 호출자의 도메인 ID(게임 세션 ID, 채팅방 ID 등)
|
|
226
|
+
멀티턴 대화에서 `sessionKey`로 호출자의 도메인 ID(게임 세션 ID, 채팅방 ID 등)를 넘기면 SDK가 model 범위의 불투명 affinity key를 파생합니다. Qgrid는 이를 `prompt_cache_key`로 보내고 매 요청 전체 대화 history를 재전송합니다. Provider thread나 process session은 보관하지 않습니다.
|
|
221
227
|
|
|
222
228
|
```typescript
|
|
223
229
|
const { text } = await generateText({
|
|
@@ -227,8 +233,8 @@ const { text } = await generateText({
|
|
|
227
233
|
});
|
|
228
234
|
```
|
|
229
235
|
|
|
230
|
-
-
|
|
231
|
-
- `anthropic/*` 모델에서는
|
|
236
|
+
- SDK 내부 affinity coordinate entry는 idle 10분 후 만료됩니다. 동일 affinity key 파생은 이 entry에 의존하지 않습니다.
|
|
237
|
+
- `anthropic/*` 모델에서는 무시됩니다. Claude Code는 자체 prefix-cache 동작을 사용합니다.
|
|
232
238
|
|
|
233
239
|
### Image Generation
|
|
234
240
|
|
|
@@ -268,7 +274,7 @@ const result = await generateText({
|
|
|
268
274
|
```
|
|
269
275
|
|
|
270
276
|
- `streamText`에서는 거부됩니다 (non-stream 전용).
|
|
271
|
-
- 이미지 생성 요청은
|
|
277
|
+
- 이미지 생성 요청은 provider 대화 상태를 보관하지 않고 전체 input을 직접 전송합니다.
|
|
272
278
|
- 레퍼런스 이미지는 JSON data URL로 전송됩니다. 큰 사진은 압축하거나 리사이즈해서 전달하세요. 과도하게 큰 base64 입력은 SDK가 거부하며, 사진에는 WebP/JPEG를 권장합니다.
|
|
273
279
|
- 이미지 비용은 `gpt-image-2` 공개 단가표 기반 **추정치**로 request log의 `image_cost_usd`에 별도 기록됩니다 (codex가 정확한 이미지 tool 사용량을 노출하지 않음).
|
|
274
280
|
|
|
@@ -340,7 +346,7 @@ AI SDK response metadata로 실제 serving 모델을 다르게 보고하면 step
|
|
|
340
346
|
|
|
341
347
|
```typescript
|
|
342
348
|
type QgridSupportedModel =
|
|
343
|
-
// OpenAI (
|
|
349
|
+
// OpenAI (direct private Codex Responses backend)
|
|
344
350
|
| "openai/gpt-5.6-sol"
|
|
345
351
|
| "openai/gpt-5.6-terra"
|
|
346
352
|
| "openai/gpt-5.6-luna"
|
|
@@ -369,13 +375,13 @@ type QgridSupportedModel =
|
|
|
369
375
|
|
|
370
376
|
### GPT-5.6 사양
|
|
371
377
|
|
|
372
|
-
| 모델 | Context (qgrid
|
|
378
|
+
| 모델 | Context (qgrid OpenAI 경로) | 최대 출력 | 1M tokens당 input / cached input / output |
|
|
373
379
|
|---|---:|---:|---:|
|
|
374
380
|
| `openai/gpt-5.6-sol` | 372K | 128K | $5 / $0.50 / $30 |
|
|
375
381
|
| `openai/gpt-5.6-terra` | 372K | 128K | $2.50 / $0.25 / $15 |
|
|
376
382
|
| `openai/gpt-5.6-luna` | 372K | 128K | $1 / $0.10 / $6 |
|
|
377
383
|
|
|
378
|
-
세 모델 모두 `max` reasoning effort를 지원합니다.
|
|
384
|
+
세 모델 모두 `max` reasoning effort를 지원합니다. Qgrid는 model 설정에 사용해 온 구독 경로의 관측 한도인 372K context window(95% effective, 실사용 입력 약 353K)와 128K 최대 출력을 유지합니다. 이는 OpenAI public API에 표시된 1.05M context보다 작으며 local runtime의 제약으로 귀속하지 않습니다. 입력이 272K tokens를 넘으면 요청 전체에 input 2x, output 1.5x 장문 컨텍스트 할증이 적용되며, cache write는 uncached input 단가의 1.25x입니다.
|
|
379
385
|
|
|
380
386
|
`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을 보존합니다.
|
|
381
387
|
|
|
@@ -406,7 +412,7 @@ qgrid(modelId, {
|
|
|
406
412
|
|
|
407
413
|
## 주의사항
|
|
408
414
|
|
|
409
|
-
- `temperature`, `maxOutputTokens` 등 sampling 파라미터는
|
|
415
|
+
- `temperature`, `maxOutputTokens` 등 sampling 파라미터는 OpenAI private Codex 경로와 Anthropic Claude Code 경로가 qgrid를 통해 받지 않으므로 무시됩니다.
|
|
410
416
|
- Structured output은 top-level `object` schema만 서버에서 강제됩니다. top-level `array`는 클라이언트 파싱 fallback.
|
|
411
417
|
- `tools`와 `Output.object`를 함께 사용하려면 qgrid server와 AI SDK가 모두 2.5.4 이상이어야 합니다.
|
|
412
418
|
- AI SDK/Zod가 Draft-7 `items: [...]`로 만드는 위치 기반 tuple은 지원되는
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
AI SDK v6 custom `LanguageModelV3` provider for [qgrid](https://github.com/cartanova-ai/Qgrid).
|
|
6
6
|
|
|
7
|
-
**Without changing your existing AI SDK code, swap a single `model` line to get subscription-token pooling (N tokens ×
|
|
7
|
+
**Without changing your existing AI SDK code, swap a single `model` line to get subscription-token pooling (N tokens × concurrent permits) + the request log dashboard.**
|
|
8
8
|
|
|
9
9
|
```diff
|
|
10
10
|
import { generateText } from "ai";
|
|
@@ -50,6 +50,8 @@ const { text } = await generateText({
|
|
|
50
50
|
|
|
51
51
|
A qgrid server (`http://localhost:44900`) must be running.
|
|
52
52
|
|
|
53
|
+
The OpenAI server route calls the private ChatGPT Codex Responses backend directly over HTTPS/SSE. That backend is undocumented and may change without notice; qgrid's mocked protocol tests are not live-provider verification.
|
|
54
|
+
|
|
53
55
|
## Usage
|
|
54
56
|
> Before you start: all client-side usage is identical to the [AI SDK](https://ai-sdk.dev/docs/ai-sdk-core).
|
|
55
57
|
|
|
@@ -90,10 +92,18 @@ const { output } = await generateText({
|
|
|
90
92
|
console.log(output.title, output.authors);
|
|
91
93
|
```
|
|
92
94
|
|
|
93
|
-
Schemas with a top-level `object` are forwarded to server
|
|
94
|
-
|
|
95
|
+
Schemas with a top-level `object` are forwarded to the server. If the top level is not an
|
|
96
|
+
`object` (e.g. array), the AI SDK falls back to client-side parsing and a warning is logged.
|
|
95
97
|
|
|
96
|
-
> **Note for Anthropic models:**
|
|
98
|
+
> **Note for Anthropic models:** enforcement differs by provider. OpenAI/codex constrains
|
|
99
|
+
> decoding to the schema, so non-conforming output is rare. The Anthropic route has no
|
|
100
|
+
> enforcement mechanism: qgrid delivers your original schema as prompt guidance at the end of
|
|
101
|
+
> the system prompt, Claude Code generates plain text, and the server only strips code fences.
|
|
102
|
+
> The reply is schema-guided JSON text, **not server-validated JSON** — validation happens in
|
|
103
|
+
> this SDK via your zod schema (`Output.object` does this automatically), and a non-conforming
|
|
104
|
+
> reply surfaces as an explicit validation error on your side instead of hidden server retries.
|
|
105
|
+
> Raw HTTP consumers calling the qgrid API without this SDK must validate the response
|
|
106
|
+
> themselves.
|
|
97
107
|
|
|
98
108
|
### Streaming
|
|
99
109
|
|
|
@@ -201,12 +211,12 @@ const { text } = await generateText({
|
|
|
201
211
|
| Option | Values | Applies to | Description |
|
|
202
212
|
|---|---|---|---|
|
|
203
213
|
| `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 |
|
|
204
|
-
| `sessionKey` | `string` | OpenAI only | Multi-turn conversation identifier
|
|
214
|
+
| `sessionKey` | `string` | OpenAI only | Multi-turn conversation identifier used to derive opaque prompt-cache affinity while replaying full history (see [below](#multi-turn-prompt-cache-sessionkey)) |
|
|
205
215
|
| `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"`) |
|
|
206
216
|
| `verbosity` | `"low"` \| `"medium"` \| `"high"` | OpenAI only | Response text verbosity |
|
|
207
217
|
| `reasoningSummary` | `"auto"` \| `"concise"` \| `"detailed"` \| `"none"` | OpenAI only | Reasoning summary output mode |
|
|
208
218
|
| `serviceTier` | `string` | OpenAI only | OpenAI/codex service tier |
|
|
209
|
-
| `timeoutMs` | positive integer, max `1_800_000` | Anthropic only | Server-side Claude Code process timeout in milliseconds. Defaults to 240 seconds |
|
|
219
|
+
| `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 |
|
|
210
220
|
| `imageGeneration` | `boolean` | OpenAI only, non-stream | Enables codex's built-in `image_generation` tool (see [below](#image-generation)) |
|
|
211
221
|
| `imageGenerationOptions` | `{ quality?, size? }` | OpenAI only | Image quality/size hints. `quality: "low" \| "medium" \| "high"`, `size: "1024x1024" \| "1024x1536" \| "1536x1024"` (defaults: `medium` / `1536x1024`) |
|
|
212
222
|
| `fallbackModels` | `string[]` | reserved | Reserved for future qgrid server-side fallback routing. Not functional yet and unrelated to Claude Code's Fable refusal fallback |
|
|
@@ -214,12 +224,15 @@ const { text } = await generateText({
|
|
|
214
224
|
AI SDK's top-level `timeout` remains the overall client-side budget and is converted to an
|
|
215
225
|
`AbortSignal` before the custom provider runs. It does not expose the numeric timeout to qgrid.
|
|
216
226
|
Use `providerOptions.qgrid.timeoutMs` when the qgrid server's Claude Code process needs a different
|
|
217
|
-
limit.
|
|
218
|
-
|
|
227
|
+
limit. Anthropic `generateText` requests use a request-scoped Undici dispatcher without changing global process
|
|
228
|
+
state; its `headersTimeout` and `bodyTimeout` are set to `timeoutMs + 60_000`. For example, a
|
|
229
|
+
600-second server limit gets a 660-second HTTP transport budget, allowing the server's explicit
|
|
230
|
+
timeout response to arrive before the client transport gives up. A client abort or disconnected
|
|
231
|
+
non-streaming HTTP request also terminates the server-side provider execution.
|
|
219
232
|
|
|
220
233
|
### Multi-turn prompt cache (sessionKey)
|
|
221
234
|
|
|
222
|
-
For multi-turn conversations, pass a
|
|
235
|
+
For multi-turn conversations, pass a caller-side domain ID (game session ID, chat room ID, ...) as `sessionKey`. The SDK derives a model-scoped opaque affinity key; qgrid sends it as `prompt_cache_key` and replays the complete conversation history on every request. No provider thread or process session is retained.
|
|
223
236
|
|
|
224
237
|
```typescript
|
|
225
238
|
const { text } = await generateText({
|
|
@@ -229,8 +242,8 @@ const { text } = await generateText({
|
|
|
229
242
|
});
|
|
230
243
|
```
|
|
231
244
|
|
|
232
|
-
-
|
|
233
|
-
- Ignored for `anthropic/*` models
|
|
245
|
+
- The SDK's internal affinity-coordinate entry expires after 10 minutes idle; deriving the same affinity key does not depend on that entry.
|
|
246
|
+
- Ignored for `anthropic/*` models; Claude Code uses its own prefix-cache behavior.
|
|
234
247
|
|
|
235
248
|
### Image Generation
|
|
236
249
|
|
|
@@ -270,7 +283,7 @@ const result = await generateText({
|
|
|
270
283
|
```
|
|
271
284
|
|
|
272
285
|
- Rejected in `streamText` (non-stream only).
|
|
273
|
-
- Image-generation requests
|
|
286
|
+
- Image-generation requests do not retain provider conversation state. Their full input is sent directly.
|
|
274
287
|
- Reference images are sent as JSON data URLs. Compress or resize large photos before passing them in; oversized base64 inputs are rejected by the SDK. WebP/JPEG is recommended for photos.
|
|
275
288
|
- 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).
|
|
276
289
|
|
|
@@ -343,7 +356,7 @@ The `qgrid()` provider has its own lifecycle, so the logger automatically suppre
|
|
|
343
356
|
|
|
344
357
|
```typescript
|
|
345
358
|
type QgridSupportedModel =
|
|
346
|
-
// OpenAI (
|
|
359
|
+
// OpenAI (direct private Codex Responses backend)
|
|
347
360
|
| "openai/gpt-5.6-sol"
|
|
348
361
|
| "openai/gpt-5.6-terra"
|
|
349
362
|
| "openai/gpt-5.6-luna"
|
|
@@ -372,13 +385,13 @@ type QgridSupportedModel =
|
|
|
372
385
|
|
|
373
386
|
### GPT-5.6 specifications
|
|
374
387
|
|
|
375
|
-
| Model | Context (qgrid
|
|
388
|
+
| Model | Context (qgrid OpenAI route) | Max output | Input / cached input / output per 1M tokens |
|
|
376
389
|
|---|---:|---:|---:|
|
|
377
390
|
| `openai/gpt-5.6-sol` | 372K | 128K | $5 / $0.50 / $30 |
|
|
378
391
|
| `openai/gpt-5.6-terra` | 372K | 128K | $2.50 / $0.25 / $15 |
|
|
379
392
|
| `openai/gpt-5.6-luna` | 372K | 128K | $1 / $0.10 / $6 |
|
|
380
393
|
|
|
381
|
-
All GPT-5.6 models support reasoning through `max`.
|
|
394
|
+
All GPT-5.6 models support reasoning through `max`. Qgrid retains the observed subscription-route limits used by its model configuration: a 372K context window (95% effective — about 353K of usable input) and 128K maximum output. This is narrower than the 1.05M context listed for the public OpenAI API and is not attributed to a local runtime. 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.
|
|
382
395
|
|
|
383
396
|
`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.
|
|
384
397
|
|
|
@@ -409,7 +422,7 @@ If multiple projects/workflows share one qgrid server, set `QGRID_PROJECT_NAME`.
|
|
|
409
422
|
|
|
410
423
|
## Notes
|
|
411
424
|
|
|
412
|
-
- Sampling parameters such as `temperature` and `maxOutputTokens` are ignored
|
|
425
|
+
- Sampling parameters such as `temperature` and `maxOutputTokens` are ignored because the OpenAI private Codex route and the Anthropic Claude Code route do not accept them through qgrid.
|
|
413
426
|
- Structured output is server-enforced only for top-level `object` schemas. Top-level `array` falls back to client-side parsing.
|
|
414
427
|
- Combining `tools` with `Output.object` requires qgrid server and AI SDK 2.5.4 or later.
|
|
415
428
|
- AI SDK/Zod positional tuples emitted as Draft-7 `items: [...]` are normalized
|
package/dist/index.d.ts
CHANGED
|
@@ -18,23 +18,24 @@ type QgridProviderOptions = {
|
|
|
18
18
|
*/
|
|
19
19
|
logger?: boolean;
|
|
20
20
|
/**
|
|
21
|
-
* 멀티턴
|
|
22
|
-
* provider가
|
|
23
|
-
* 좌표는 sessionKey 별로 격리된다.
|
|
21
|
+
* 멀티턴 프롬프트 캐시 어피니티용 대화 식별자, 호출자가 자기 도메인 ID(예: 게임 세션 ID) 하나만 넘기면
|
|
22
|
+
* provider가 model+sessionKey에서 opaque cache affinity를 결정적으로 파생해 회송한다.
|
|
23
|
+
* 원문 sessionKey는 서버에 전송하지 않으며 좌표는 model+sessionKey 별로 격리된다.
|
|
24
24
|
*/
|
|
25
25
|
sessionKey?: string;
|
|
26
26
|
/** reasoning 모델의 추론 깊이. 기본값은 qgrid config의 defaultEffort. */
|
|
27
27
|
effort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
28
|
-
/** 응답 텍스트의 상세도. OpenAI
|
|
28
|
+
/** 응답 텍스트의 상세도. OpenAI route에서만 적용된다. */
|
|
29
29
|
verbosity?: "low" | "medium" | "high";
|
|
30
|
-
/** reasoning 모델의 추론 요약 출력 방식. OpenAI
|
|
30
|
+
/** reasoning 모델의 추론 요약 출력 방식. OpenAI route에서만 적용된다. */
|
|
31
31
|
reasoningSummary?: "auto" | "concise" | "detailed" | "none";
|
|
32
|
-
/** OpenAI
|
|
32
|
+
/** OpenAI service tier. OpenAI route에서만 적용된다. */
|
|
33
33
|
serviceTier?: string;
|
|
34
34
|
/**
|
|
35
35
|
* qgrid 서버의 provider 실행 제한(ms). Anthropic 경로에서는 Claude Code 프로세스 타이머로
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* 사용되며, SDK의 non-stream 요청별 HTTP headers/body timeout은 이 값보다 60초 길게 설정된다.
|
|
37
|
+
* AI SDK 표준 timeout은 provider에 숫자를 전달하지 않고 abortSignal로 변환되므로, 서버 측
|
|
38
|
+
* 제한을 바꾸려면 이 옵션을 사용한다. 양의 정수만 허용하며 최대 30분, 기본 240초다.
|
|
38
39
|
*/
|
|
39
40
|
timeoutMs?: number;
|
|
40
41
|
/**
|
|
@@ -43,7 +44,7 @@ type QgridProviderOptions = {
|
|
|
43
44
|
*/
|
|
44
45
|
fallbackModels?: string[];
|
|
45
46
|
/**
|
|
46
|
-
*
|
|
47
|
+
* OpenAI Responses image_generation tool 을 켠다. OpenAI route + non-stream 전용.
|
|
47
48
|
* generateText 결과의 files 로 이미지를 받는다. streaming(streamText)에서는 거부된다.
|
|
48
49
|
*/
|
|
49
50
|
imageGeneration?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Agent } from "undici";
|
|
1
2
|
//#region src/envelope-stream-parser.ts
|
|
2
3
|
const ESCAPE_MAP = {
|
|
3
4
|
"\"": "\"",
|
|
@@ -252,6 +253,87 @@ function createEnvelopeStreamParser(answerKind) {
|
|
|
252
253
|
} };
|
|
253
254
|
}
|
|
254
255
|
//#endregion
|
|
256
|
+
//#region src/http-transport.ts
|
|
257
|
+
const DEFAULT_ANTHROPIC_TIMEOUT_MS = 24e4;
|
|
258
|
+
const TRANSPORT_RESPONSE_GRACE_MS = 6e4;
|
|
259
|
+
function anthropicTransportOptions(timeoutMs) {
|
|
260
|
+
const transportTimeoutMs = (Number.isFinite(timeoutMs) && timeoutMs !== void 0 && timeoutMs > 0 ? timeoutMs : DEFAULT_ANTHROPIC_TIMEOUT_MS) + TRANSPORT_RESPONSE_GRACE_MS;
|
|
261
|
+
return {
|
|
262
|
+
headersTimeout: transportTimeoutMs,
|
|
263
|
+
bodyTimeout: transportTimeoutMs
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function createQgridRequestTransport(modelId, timeoutMs) {
|
|
267
|
+
if (!modelId.startsWith("anthropic/")) return void 0;
|
|
268
|
+
const options = anthropicTransportOptions(timeoutMs);
|
|
269
|
+
return {
|
|
270
|
+
dispatcher: new Agent(options),
|
|
271
|
+
timeoutMs: options.headersTimeout
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
async function closeQgridRequestTransport(transport) {
|
|
275
|
+
if (!transport) return;
|
|
276
|
+
await transport.dispatcher.close().catch(() => void 0);
|
|
277
|
+
}
|
|
278
|
+
async function qgridFetch(input, init, context) {
|
|
279
|
+
try {
|
|
280
|
+
return await fetch(input, init);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
throw toQgridTransportError(error, context);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function toQgridTransportError(error, context) {
|
|
286
|
+
if (isAbortError(error)) return error;
|
|
287
|
+
const code = findErrorCode(error);
|
|
288
|
+
const prefix = `qgrid ${context.operation} transport failed`;
|
|
289
|
+
if (code === "UND_ERR_HEADERS_TIMEOUT") {
|
|
290
|
+
const budget = context.transportTimeoutMs ? ` after ${context.transportTimeoutMs}ms` : "";
|
|
291
|
+
return new Error(`${prefix}: response headers timed out${budget} (${code})`, { cause: error });
|
|
292
|
+
}
|
|
293
|
+
if (code === "UND_ERR_BODY_TIMEOUT") {
|
|
294
|
+
const budget = context.transportTimeoutMs ? ` after ${context.transportTimeoutMs}ms` : "";
|
|
295
|
+
return new Error(`${prefix}: response body timed out${budget} (${code})`, { cause: error });
|
|
296
|
+
}
|
|
297
|
+
if (code === "ECONNREFUSED") return new Error(`${prefix}: connection refused by ${serverOrigin(context.serverUrl)} (${code})`, { cause: error });
|
|
298
|
+
const detail = findDeepestMessage(error) ?? "unknown network error";
|
|
299
|
+
return new Error(`${prefix}: ${detail}${code ? ` (${code})` : ""}`, { cause: error });
|
|
300
|
+
}
|
|
301
|
+
function isAbortError(error) {
|
|
302
|
+
return error instanceof Error && error.name === "AbortError";
|
|
303
|
+
}
|
|
304
|
+
function findErrorCode(error, seen = /* @__PURE__ */ new Set()) {
|
|
305
|
+
if (!isErrorLike(error) || seen.has(error)) return void 0;
|
|
306
|
+
seen.add(error);
|
|
307
|
+
if (typeof error.code === "string") return error.code;
|
|
308
|
+
const causeCode = findErrorCode(error.cause, seen);
|
|
309
|
+
if (causeCode) return causeCode;
|
|
310
|
+
if (Array.isArray(error.errors)) for (const nested of error.errors) {
|
|
311
|
+
const nestedCode = findErrorCode(nested, seen);
|
|
312
|
+
if (nestedCode) return nestedCode;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function findDeepestMessage(error, seen = /* @__PURE__ */ new Set()) {
|
|
316
|
+
if (!isErrorLike(error) || seen.has(error)) return void 0;
|
|
317
|
+
seen.add(error);
|
|
318
|
+
const causeMessage = findDeepestMessage(error.cause, seen);
|
|
319
|
+
if (causeMessage) return causeMessage;
|
|
320
|
+
if (Array.isArray(error.errors)) for (const nested of error.errors) {
|
|
321
|
+
const nestedMessage = findDeepestMessage(nested, seen);
|
|
322
|
+
if (nestedMessage) return nestedMessage;
|
|
323
|
+
}
|
|
324
|
+
return typeof error.message === "string" && error.message.length > 0 ? error.message : void 0;
|
|
325
|
+
}
|
|
326
|
+
function isErrorLike(value) {
|
|
327
|
+
return typeof value === "object" && value !== null;
|
|
328
|
+
}
|
|
329
|
+
function serverOrigin(serverUrl) {
|
|
330
|
+
try {
|
|
331
|
+
return new URL(serverUrl).origin;
|
|
332
|
+
} catch {
|
|
333
|
+
return serverUrl;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
//#endregion
|
|
255
337
|
//#region src/qgrid.constant.ts
|
|
256
338
|
const QGRID_PROVIDER_NAME = "qgrid";
|
|
257
339
|
const THREAD_COORD_TTL_MS = 10 * 6e4;
|
|
@@ -384,7 +466,7 @@ function extractPromptAndHistory(messages, options = {}) {
|
|
|
384
466
|
const extracted = extractUserContent(msg.content, includeImages);
|
|
385
467
|
imageUrls.push(...extracted.imageUrls);
|
|
386
468
|
droppedImageCount += extracted.droppedImageCount;
|
|
387
|
-
const content =
|
|
469
|
+
const content = responsesContentItemsFromExtracted(extracted.parts);
|
|
388
470
|
history.push({
|
|
389
471
|
type: "message",
|
|
390
472
|
role: "user",
|
|
@@ -495,7 +577,7 @@ function inputPartsFromExtracted(extractedParts) {
|
|
|
495
577
|
});
|
|
496
578
|
return parts.some((part) => part.type === "image") ? parts : void 0;
|
|
497
579
|
}
|
|
498
|
-
function
|
|
580
|
+
function responsesContentItemsFromExtracted(parts) {
|
|
499
581
|
const items = [];
|
|
500
582
|
for (const part of parts) if (part.kind === "text") items.push({
|
|
501
583
|
type: "input_text",
|
|
@@ -917,6 +999,7 @@ function createQgridLogger(config = {}) {
|
|
|
917
999
|
//#endregion
|
|
918
1000
|
//#region src/index.ts
|
|
919
1001
|
const threadCoordStore = /* @__PURE__ */ new Map();
|
|
1002
|
+
const CACHE_AFFINITY_NAMESPACE = "qgrid-cache-affinity-v1";
|
|
920
1003
|
const MAX_IMAGE_INPUT_DATA_URL_CHARS = 9e6;
|
|
921
1004
|
const MAX_IMAGE_INPUT_DATA_URL_CHARS_LABEL = "9M";
|
|
922
1005
|
function assertImageInputFitsJsonTransport(imageUrls) {
|
|
@@ -957,6 +1040,12 @@ function setThreadCoord(sessionKey, coord) {
|
|
|
957
1040
|
expiresAt: Date.now() + THREAD_COORD_TTL_MS
|
|
958
1041
|
});
|
|
959
1042
|
}
|
|
1043
|
+
async function deriveCacheAffinityKey(modelId, sessionKey) {
|
|
1044
|
+
if (!sessionKey || modelId.startsWith("anthropic/")) return void 0;
|
|
1045
|
+
const bytes = new TextEncoder().encode(`${CACHE_AFFINITY_NAMESPACE}\0${modelId}\0${sessionKey}`);
|
|
1046
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
1047
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1048
|
+
}
|
|
960
1049
|
function qgridProviderMetadata(data) {
|
|
961
1050
|
return { qgrid: {
|
|
962
1051
|
model: data.model,
|
|
@@ -970,15 +1059,13 @@ function qgridProviderMetadata(data) {
|
|
|
970
1059
|
cacheCreation1hInputTokens: data.usage.cache_creation_1h_input_tokens ?? null
|
|
971
1060
|
} };
|
|
972
1061
|
}
|
|
973
|
-
function reusableSessionKey(modelId, sessionKey) {
|
|
974
|
-
return modelId.startsWith("anthropic/") ? void 0 : sessionKey;
|
|
975
|
-
}
|
|
976
1062
|
function getQgridProviderOptions(options) {
|
|
977
1063
|
return options.providerOptions?.qgrid ?? {};
|
|
978
1064
|
}
|
|
979
1065
|
function toAiSdkUsage(usage) {
|
|
980
1066
|
const cacheRead = usage.cache_read_input_tokens;
|
|
981
1067
|
const cacheWrite = usage.cache_creation_input_tokens;
|
|
1068
|
+
const reasoning = usage.reasoning_tokens ?? 0;
|
|
982
1069
|
return {
|
|
983
1070
|
inputTokens: {
|
|
984
1071
|
total: usage.input_tokens,
|
|
@@ -988,8 +1075,8 @@ function toAiSdkUsage(usage) {
|
|
|
988
1075
|
},
|
|
989
1076
|
outputTokens: {
|
|
990
1077
|
total: usage.output_tokens,
|
|
991
|
-
text: usage.output_tokens,
|
|
992
|
-
reasoning
|
|
1078
|
+
text: Math.max(usage.output_tokens - reasoning, 0),
|
|
1079
|
+
reasoning
|
|
993
1080
|
}
|
|
994
1081
|
};
|
|
995
1082
|
}
|
|
@@ -1051,7 +1138,7 @@ function qgrid(modelId, config) {
|
|
|
1051
1138
|
const imageGenerationOptions = qgridOptions.imageGenerationOptions;
|
|
1052
1139
|
if (imageGeneration) assertImageInputFitsJsonTransport(imageUrls);
|
|
1053
1140
|
const sessionKey = qgridOptions.sessionKey;
|
|
1054
|
-
const
|
|
1141
|
+
const cacheAffinityKey = imageGeneration ? void 0 : await deriveCacheAffinityKey(modelId, sessionKey);
|
|
1055
1142
|
const jsonSchema = serializeObjectResponseFormat(options.responseFormat);
|
|
1056
1143
|
let runContext;
|
|
1057
1144
|
let toolResultsPayload;
|
|
@@ -1060,9 +1147,10 @@ function qgrid(modelId, config) {
|
|
|
1060
1147
|
runContext = matchedClientRun.runContext;
|
|
1061
1148
|
toolResultsPayload = matchedClientRun.toolResultsPayload;
|
|
1062
1149
|
}
|
|
1063
|
-
const storedCoord =
|
|
1150
|
+
const storedCoord = cacheAffinityKey ? getThreadCoord(cacheAffinityKey) : void 0;
|
|
1064
1151
|
if (!matchedClientRun && storedCoord) runContext = { threadCoord: storedCoord };
|
|
1065
|
-
const
|
|
1152
|
+
const transport = createQgridRequestTransport(modelId, timeoutMs);
|
|
1153
|
+
const data = await qgridFetch(`${serverUrl}/api/qgrid/query`, {
|
|
1066
1154
|
method: "POST",
|
|
1067
1155
|
headers: { "Content-Type": "application/json" },
|
|
1068
1156
|
body: JSON.stringify({ args: {
|
|
@@ -1079,20 +1167,26 @@ function qgrid(modelId, config) {
|
|
|
1079
1167
|
...jsonSchema ? { jsonSchema } : {},
|
|
1080
1168
|
...history.length > 0 ? { history: JSON.stringify(history) } : {},
|
|
1081
1169
|
...projectName ? { projectName } : {},
|
|
1170
|
+
...cacheAffinityKey ? { cacheAffinityKey } : {},
|
|
1082
1171
|
...logger === false ? { logger: false } : {},
|
|
1083
1172
|
...runContext ? { runContext } : {},
|
|
1084
1173
|
...toolResultsPayload ? { toolResults: toolResultsPayload } : {},
|
|
1085
1174
|
...imageGeneration ? { imageGeneration } : {},
|
|
1086
1175
|
...imageGenerationOptions ? { imageGenerationOptions } : {}
|
|
1087
1176
|
} }),
|
|
1088
|
-
signal: options.abortSignal
|
|
1177
|
+
signal: options.abortSignal,
|
|
1178
|
+
...transport ? { dispatcher: transport.dispatcher } : {}
|
|
1179
|
+
}, {
|
|
1180
|
+
operation: "query",
|
|
1181
|
+
serverUrl,
|
|
1182
|
+
transportTimeoutMs: transport?.timeoutMs
|
|
1089
1183
|
}).then(async (res) => {
|
|
1090
1184
|
if (!res.ok) {
|
|
1091
1185
|
const text = await res.text().catch(() => "");
|
|
1092
1186
|
throw new Error(`qgrid ${res.status}: ${text}`);
|
|
1093
1187
|
}
|
|
1094
1188
|
return await res.json();
|
|
1095
|
-
});
|
|
1189
|
+
}).finally(() => closeQgridRequestTransport(transport));
|
|
1096
1190
|
const content = [];
|
|
1097
1191
|
let finishReason = {
|
|
1098
1192
|
unified: "stop",
|
|
@@ -1125,7 +1219,7 @@ function qgrid(modelId, config) {
|
|
|
1125
1219
|
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.");
|
|
1126
1220
|
if (matchedClientRun) clientRuns.delete(matchedClientRun.key);
|
|
1127
1221
|
if (finishReason.unified === "tool-calls") rememberClientRun(data.runContext, content.filter((c) => c.type === "tool-call").map((c) => c.toolCallId));
|
|
1128
|
-
if (
|
|
1222
|
+
if (!imageGeneration && cacheAffinityKey && data.runContext?.threadCoord) setThreadCoord(cacheAffinityKey, data.runContext.threadCoord);
|
|
1129
1223
|
return {
|
|
1130
1224
|
content,
|
|
1131
1225
|
finishReason,
|
|
@@ -1149,7 +1243,7 @@ function qgrid(modelId, config) {
|
|
|
1149
1243
|
const { prompt, system, history, droppedImageCount } = extractPromptAndHistory(options.prompt, { includeImages: false });
|
|
1150
1244
|
warnDroppedImages(droppedImageCount);
|
|
1151
1245
|
const sessionKey = qgridOptions.sessionKey;
|
|
1152
|
-
const
|
|
1246
|
+
const cacheAffinityKey = await deriveCacheAffinityKey(modelId, sessionKey);
|
|
1153
1247
|
const jsonSchema = serializeObjectResponseFormat(options.responseFormat);
|
|
1154
1248
|
let runContext;
|
|
1155
1249
|
let toolResultsPayload;
|
|
@@ -1158,7 +1252,7 @@ function qgrid(modelId, config) {
|
|
|
1158
1252
|
runContext = matchedClientRun.runContext;
|
|
1159
1253
|
toolResultsPayload = matchedClientRun.toolResultsPayload;
|
|
1160
1254
|
}
|
|
1161
|
-
const storedCoord =
|
|
1255
|
+
const storedCoord = cacheAffinityKey ? getThreadCoord(cacheAffinityKey) : void 0;
|
|
1162
1256
|
if (!matchedClientRun && storedCoord) runContext = { threadCoord: storedCoord };
|
|
1163
1257
|
const prepRes = await fetch(`${serverUrl}/api/qgrid/prepareStream`, {
|
|
1164
1258
|
method: "POST",
|
|
@@ -1176,6 +1270,7 @@ function qgrid(modelId, config) {
|
|
|
1176
1270
|
...jsonSchema ? { jsonSchema } : {},
|
|
1177
1271
|
...history.length > 0 ? { history: JSON.stringify(history) } : {},
|
|
1178
1272
|
...projectName ? { projectName } : {},
|
|
1273
|
+
...cacheAffinityKey ? { cacheAffinityKey } : {},
|
|
1179
1274
|
...logger === false ? { logger: false } : {},
|
|
1180
1275
|
...runContext ? { runContext } : {},
|
|
1181
1276
|
...toolResultsPayload ? { toolResults: toolResultsPayload } : {}
|
|
@@ -1228,7 +1323,7 @@ function qgrid(modelId, config) {
|
|
|
1228
1323
|
const done = event.data;
|
|
1229
1324
|
if (matchedClientRun) clientRuns.delete(matchedClientRun.key);
|
|
1230
1325
|
if (done.finishReason === "tool-calls") rememberClientRun(done.runContext, (done.content ?? []).filter((c) => c.type === "tool-call").map((c) => c.toolCallId));
|
|
1231
|
-
if (
|
|
1326
|
+
if (cacheAffinityKey && done.runContext?.threadCoord) setThreadCoord(cacheAffinityKey, done.runContext.threadCoord);
|
|
1232
1327
|
if (done.content) {
|
|
1233
1328
|
for (const item of done.content) if (item.type === "text" && !deltaTextEmitted) {
|
|
1234
1329
|
const tid = `${TEXT_STREAM_ID_PREFIX}_${Math.random().toString(36).slice(2, 10)}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cartanova/qgrid-ai-sdk",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.2",
|
|
4
4
|
"description": "AI SDK LanguageModelV3 provider for qgrid",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
"types": "./dist/index.d.ts"
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"undici": "^6.28.0"
|
|
24
|
+
},
|
|
22
25
|
"devDependencies": {
|
|
23
26
|
"@ai-sdk/provider": "^3.0.0",
|
|
24
27
|
"ai": "^6.0.190",
|