@cartanova/qgrid-ai-sdk 2.19.1 → 2.19.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 +47 -0
- package/README.md +47 -0
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -518,6 +518,53 @@ const speech = await generateSpeech({
|
|
|
518
518
|
- 음성 합성은 TTS 모델이 아니라 텍스트를 읽는 대화 모델입니다. qgrid는 `<script>` 태그 안의 텍스트만 읽고 답하지 말라고 지시합니다. 원문 그대로 읽는지는 짧은 영어 문장과 질문형 영어 캐릭터 대사에서 검증했습니다. 모델이 실제로 말한 transcript를 요청 로그에 함께 남겨 비교할 수 있지만, 한영 혼합 텍스트에서는 transcript가 일치해도 영어 음성을 알아듣기 어려운 경우가 있었습니다.
|
|
519
519
|
- 모델별 동작은 서버 저장소의 `docs/diagnosis/2026-10-17-openai-realtime-audio-probe.md`에 기록되어 있습니다. 2026-10-17 기준 알려진 한계: `gpt-live-transcribe`, `gpt-realtime-whisper`는 60초 클립의 절반 정도만 반환하므로 짧은 발화에 쓰세요. `gpt-realtime-whisper`는 `openai.prompt`를 거부합니다(서버가 업스트림 호출 전에 HTTP 400). `whisper-1`은 `openai.prompt`를 받지만 무시합니다. 음성 합성은 `gpt-realtime-2.1-mini`가 모든 테스트 문장을 그대로 읽었고(13/13), `gpt-realtime-2.1`은 1회 의역(12/13)했습니다. `gpt-realtime-1.5`는 텍스트를 읽지 않고 답변하는 경우가 잦아(6/13) 2.18.0에서 제외했습니다. 2026-11-06에는 현재 프롬프트로 `gpt-realtime-2.1`과 `gpt-realtime-2.1-mini` 모두 질문형 캐릭터 대사 12/12를 그대로 읽었습니다(이전 프롬프트에서는 `gpt-realtime-2.1`이 7/12를 답변). 2026-11-09에 독립 STT 2종으로 영어 108건(6문장, 보이스 `marin` `cedar` `ballad`, 각 3회)을 판정한 결과 `gpt-realtime-2.1`은 54/54, `gpt-realtime-2.1-mini`는 53/54였고, 실패 1건은 "Sure, let me read that out loud for you exactly as written."으로 시작했습니다. 한영 혼합 문장은 두 모델 모두 끝까지 읽었습니다(각 9/9). 모델이 남기는 transcript에는 이런 서두가 나타나지 않으므로, 정확성이 중요하면 transcript 대신 STT나 `withTimestamps`(정렬이 덧붙은 말을 건너뜀)로 오디오를 확인하세요.
|
|
520
520
|
|
|
521
|
+
### OpenAI 실시간 음성 대화 (GPT-Live)
|
|
522
|
+
|
|
523
|
+
서버 2.19.2+는 ChatGPT 구독 토큰으로 `openai/gpt-live-1` 대화를 엽니다. GPT-Live는 듣는 동안에도 말하는 대화 모델이라 음성 합성과는 다릅니다. AI SDK에 해당 인터페이스가 없어서 이 패키지도 따로 제공하지 않습니다. 앱이 qgrid HTTP 엔드포인트를 직접 부릅니다.
|
|
524
|
+
|
|
525
|
+
오디오는 qgrid를 지나지 않습니다. 브라우저의 WebRTC 피어가 SDP offer를 만들고, 앱 서버가 이를 `POST /api/qgrid/liveCall`로 보낸 뒤, 돌아온 SDP answer를 브라우저에 적용합니다. 그 뒤 오디오와 이벤트는 브라우저와 OpenAI가 직접 주고받습니다. qgrid는 세션이 끝날 때 대화 텍스트와 세션 오디오 시간을 로그에 남기고, 오디오는 저장하지 않습니다.
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
// 브라우저
|
|
529
|
+
const pc = new RTCPeerConnection();
|
|
530
|
+
const mic = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
531
|
+
mic.getTracks().forEach((track) => pc.addTrack(track));
|
|
532
|
+
pc.addEventListener("track", (event) => {
|
|
533
|
+
speaker.srcObject = new MediaStream([event.track]); // <audio autoplay> 요소
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
const events = pc.createDataChannel("oai-events");
|
|
537
|
+
events.addEventListener("message", (event) => console.log(JSON.parse(event.data)));
|
|
538
|
+
|
|
539
|
+
const offer = await pc.createOffer();
|
|
540
|
+
await pc.setLocalDescription(offer);
|
|
541
|
+
const { sdp } = await fetch("/live", { method: "POST", body: offer.sdp }).then((r) => r.json());
|
|
542
|
+
await pc.setRemoteDescription({ type: "answer", sdp });
|
|
543
|
+
|
|
544
|
+
// 종료: 피어만 닫으면 세션과 과금 시간이 약 30초 더 이어집니다.
|
|
545
|
+
events.send(JSON.stringify({ type: "session.close" }));
|
|
546
|
+
|
|
547
|
+
// 앱 서버
|
|
548
|
+
app.post("/live", async (req, res) => {
|
|
549
|
+
const response = await fetch(`${QGRID_URL}/api/qgrid/liveCall`, {
|
|
550
|
+
method: "POST",
|
|
551
|
+
headers: { "Content-Type": "application/json" },
|
|
552
|
+
body: JSON.stringify({
|
|
553
|
+
params: { sdp: req.body, voice: "cove", instructions: "You are a friendly English tutor.", projectName: "my-app" },
|
|
554
|
+
}),
|
|
555
|
+
});
|
|
556
|
+
res.status(response.status).json(await response.json());
|
|
557
|
+
});
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
- 위임: GPT-Live는 혼자 답할 수 없는 일(검색, 추론)을 클라이언트에 넘기고, 답이 올 때까지 말을 이어갑니다. Codex가 자기 에이전트로 이 요청을 처리하듯 qgrid가 처리합니다. 지금까지의 대화와 함께 세션의 위임 모델로 qgrid query를 돌리고, 음성이 그 답을 말합니다. 세션마다 `params.delegation`으로 고릅니다. `model`(qgrid가 서비스하는 모델, 대시보드 채팅과 같은 목록인 서버 `qgrid.types.ts`의 `QGRID_MODELS`. 기본 `openai/gpt-6-luna`, 목록 밖은 HTTP 400), `effort`(기본 `low`, `medium`, `high`), `webSearch`(제공자의 hosted 웹 검색, `openai/`·`grok/`만, 기본 꺼짐). 위임이 실패하면 짧은 실패 안내를 돌려주어 음성이 기다리지 않고 그 사실을 말하게 합니다. 위임은 각각 `delegation` tool 단계로 기록되고, 백엔드 비용이 실행 비용에 더해집니다.
|
|
561
|
+
- 요청 `params`: `sdp`(offer, 필수), `voice`, `instructions`, `delegation`, 그리고 다른 엔드포인트와 같은 `tokenName`(`openai/` 접두사), `projectName`, `logger`. `model`은 생략하거나 `openai/gpt-live-1`만 됩니다. 다른 값은 HTTP 400입니다.
|
|
562
|
+
- 응답: `sdp`(answer), `callId`, `model`, `voice`, `tokenName`, `requestLogId`. 요청 로그는 세션 내내 `running`이고, 턴이 끝날 때마다 세션 시간이 붙은 대화가 갱신되어 진행 중에도 볼 수 있습니다. 세션이 끝나면 비용 추정치(세션 오디오 분당 $0.05, API 정가)와 함께 마무리됩니다.
|
|
563
|
+
- 음성: `cove`(기본) `juniper maple spruce ember vale breeze arbor sol aube bubbie haetsal hanul himari leher miko neer porto rindo rio rivage selva sonna tinta viola`. realtime 음성 합성용 음성(`marin` 등)은 쓸 수 없습니다. OpenAI가 call은 받아 주지만 세션이 시작되지 않기 때문에 서버가 HTTP 400으로 막습니다.
|
|
564
|
+
- 데이터 채널 이벤트: `turn.created`, `turn.delta`, `turn.done`이 말하는 대로 각 턴의 `role`과 `transcript`를 전하고, `session.usage.updated`와 `session.closed`가 `usage.audio_duration_ms`를 전합니다. 세션은 시작 2시간 뒤 스스로 끝납니다.
|
|
565
|
+
- 엔드포인트는 브라우저가 아니라 앱 서버에서 부르세요. qgrid에는 호출자 인증이 없고 CORS 헤더도 보내지 않습니다.
|
|
566
|
+
- 업스트림이 요청한 모델 이름을 무시해서 실제 모델은 확인할 수 없습니다. Codex도 qgrid도 `gpt-live-1-codex`를 보냅니다. 호출자가 정의한 도구는 열지 않았습니다. 음성의 위임은 qgrid가 고른 모델로만 처리합니다.
|
|
567
|
+
|
|
521
568
|
## Telemetry Logger
|
|
522
569
|
|
|
523
570
|
서버와 AI SDK 2.10.0부터 qgrid provider의 `generateText` 결과에서는 `result.finalStep.providerMetadata?.qgrid?.requestLogId`로 요청 로그 ID를 확인할 수 있습니다. `streamText`는 스트림 소비 후 `(await result.finalStep).providerMetadata?.qgrid?.requestLogId`로 확인합니다. 직접 API를 호출하면 `query` 응답과 스트림 `done` 이벤트의 최상위 `requestLogId`에 같은 값이 담깁니다. `logger: false` 또는 구버전 서버에서는 이 필드가 없으며, 툴 호출 후속 단계는 같은 요청 로그 ID를 공유합니다. 이 ID는 후속 요청 연결 상태가 아니며, 아래 telemetry logger를 쓰는 외부 provider에는 이 metadata가 추가되지 않습니다.
|
package/README.md
CHANGED
|
@@ -530,6 +530,53 @@ const speech = await generateSpeech({
|
|
|
530
530
|
- Speech is a conversational model reading text, not a TTS model. qgrid's prompt tells it to read only the text inside `<script>` tags and never answer it. Verbatim fidelity was verified on short English sentences and on question-form English character lines. The model's own transcript of what it said is logged with the request for comparison, but for mixed Korean/English text a matching transcript did not guarantee intelligible English audio.
|
|
531
531
|
- Per-model behavior is recorded in the server repository's `docs/diagnosis/2026-10-17-openai-realtime-audio-probe.md`. Known limits on 2026-10-17: `gpt-live-transcribe` and `gpt-realtime-whisper` returned roughly half of a 60 s clip (use them for short utterances); `gpt-realtime-whisper` rejects `openai.prompt` (the server answers HTTP 400 before any upstream call); `whisper-1` accepts but ignores `openai.prompt`. For speech, `gpt-realtime-2.1-mini` repeated every test text verbatim (13/13), `gpt-realtime-2.1` paraphrased once (12/13); `gpt-realtime-1.5` often answered the text instead of repeating it (6/13) and was removed in 2.18.0. On 2026-11-06, with the current prompt, `gpt-realtime-2.1` and `gpt-realtime-2.1-mini` both read 12/12 question-form character lines verbatim (the earlier prompt let `gpt-realtime-2.1` answer 7/12). On 2026-11-09, judged by two independent STT models over 108 English clips (6 lines, voices `marin` `cedar` `ballad`, 3 takes each), `gpt-realtime-2.1` read 54/54 and `gpt-realtime-2.1-mini` 53/54: one take began with "Sure, let me read that out loud for you exactly as written." Both read every Korean/English mixed line in full (9/9 each). The model's own logged transcript does not show such a preamble, so when fidelity matters, check the audio with STT or `withTimestamps` (its alignment skips the extra words) instead of the transcript.
|
|
532
532
|
|
|
533
|
+
### OpenAI live voice conversation (GPT-Live)
|
|
534
|
+
|
|
535
|
+
Server 2.19.2+ runs `openai/gpt-live-1` conversations on a ChatGPT subscription token. GPT-Live listens and speaks at the same time, so it is a conversation, not speech generation: AI SDK has no interface for it and this package adds none. Your app calls the qgrid HTTP endpoint directly.
|
|
536
|
+
|
|
537
|
+
The audio never passes through qgrid. The browser's own WebRTC peer makes an SDP offer, your server sends it to `POST /api/qgrid/liveCall`, and the browser applies the SDP answer that comes back. Audio and events then flow between the browser and OpenAI. qgrid logs the conversation text and the session's audio time when the session ends; it stores no audio.
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
// Browser
|
|
541
|
+
const pc = new RTCPeerConnection();
|
|
542
|
+
const mic = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
543
|
+
mic.getTracks().forEach((track) => pc.addTrack(track));
|
|
544
|
+
pc.addEventListener("track", (event) => {
|
|
545
|
+
speaker.srcObject = new MediaStream([event.track]); // an <audio autoplay> element
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
const events = pc.createDataChannel("oai-events");
|
|
549
|
+
events.addEventListener("message", (event) => console.log(JSON.parse(event.data)));
|
|
550
|
+
|
|
551
|
+
const offer = await pc.createOffer();
|
|
552
|
+
await pc.setLocalDescription(offer);
|
|
553
|
+
const { sdp } = await fetch("/live", { method: "POST", body: offer.sdp }).then((r) => r.json());
|
|
554
|
+
await pc.setRemoteDescription({ type: "answer", sdp });
|
|
555
|
+
|
|
556
|
+
// To end: closing only the peer keeps the session, and its billed time, running for about 30 s.
|
|
557
|
+
events.send(JSON.stringify({ type: "session.close" }));
|
|
558
|
+
|
|
559
|
+
// Your server
|
|
560
|
+
app.post("/live", async (req, res) => {
|
|
561
|
+
const response = await fetch(`${QGRID_URL}/api/qgrid/liveCall`, {
|
|
562
|
+
method: "POST",
|
|
563
|
+
headers: { "Content-Type": "application/json" },
|
|
564
|
+
body: JSON.stringify({
|
|
565
|
+
params: { sdp: req.body, voice: "cove", instructions: "You are a friendly English tutor.", projectName: "my-app" },
|
|
566
|
+
}),
|
|
567
|
+
});
|
|
568
|
+
res.status(response.status).json(await response.json());
|
|
569
|
+
});
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
- Delegation: GPT-Live hands what it cannot answer itself (lookups, reasoning) to the client and keeps talking until the answer arrives. qgrid answers it, as Codex answers with its own agent: it runs the request as a qgrid query on the session's delegation model, with the conversation so far, and the voice speaks the reply. Choose per session with `params.delegation`: `model` (any model qgrid serves, the list the dashboard chat offers: `QGRID_MODELS` in the server's `qgrid.types.ts`; default `openai/gpt-6-luna`; others are rejected with HTTP 400), `effort` (`low` default, `medium`, `high`) and `webSearch` (the provider's hosted web search, `openai/` and `grok/` only; off by default). A failed delegation is answered with a short failure note so the voice can say so instead of waiting. Each delegation is logged as a `delegation` tool step, and its backend cost is added to the run's cost.
|
|
573
|
+
- Request `params`: `sdp` (the offer, required), `voice`, `instructions`, `delegation`, and the usual `tokenName` (`openai/` prefix), `projectName` and `logger`. `model` may be omitted or `openai/gpt-live-1`; any other value is rejected with HTTP 400.
|
|
574
|
+
- Response: `sdp` (the answer), `callId`, `model`, `voice`, `tokenName` and `requestLogId`. The request log stays `running` for the whole session and its conversation, with each turn's session time, is rewritten after every finished turn, so it can be read while the session runs. When the session ends the run is finished with a cost estimate of $0.05 per minute of session audio (the API list price).
|
|
575
|
+
- Voices: `cove` (default) `juniper maple spruce ember vale breeze arbor sol aube bubbie haetsal hanul himari leher miko neer porto rindo rio rivage selva sonna tinta viola`. The realtime speech voices (`marin` and others) do not apply; the server rejects them with HTTP 400, because OpenAI would accept the call and then never start the session.
|
|
576
|
+
- Data channel events: `turn.created`, `turn.delta` and `turn.done` carry each turn's `role` and `transcript` as it is spoken; `session.usage.updated` and `session.closed` carry `usage.audio_duration_ms`. A session ends on its own two hours after it starts.
|
|
577
|
+
- Call the endpoint from your server, not from the browser: qgrid has no caller authentication and sends no CORS headers.
|
|
578
|
+
- The upstream ignores the requested model name, so the serving model cannot be confirmed; Codex itself sends `gpt-live-1-codex`, as qgrid does. Caller-defined tools are not exposed: the voice's delegations run on qgrid's chosen model only.
|
|
579
|
+
|
|
533
580
|
## Telemetry Logger
|
|
534
581
|
|
|
535
582
|
With qgrid server and SDK 2.10.0+, read the parent request-log ID from `result.finalStep.providerMetadata?.qgrid?.requestLogId` after `generateText`. For `streamText`, consume the stream first, then read `(await result.finalStep).providerMetadata?.qgrid?.requestLogId`. Raw API query responses and stream `done` events expose the same ID as top-level `requestLogId`. The field is omitted with `logger: false` or an older server. Tool-run steps share the parent ID; it is not continuation state. This metadata applies to the qgrid provider, not external providers using the telemetry logger below.
|
package/dist/index.d.ts
CHANGED
|
@@ -154,10 +154,10 @@ type QgridGrokModel = "grok/grok-4.7" | "grok/grok-4.7-build-fast" | "grok/grok-
|
|
|
154
154
|
*/
|
|
155
155
|
declare const qgridXaiProviderOptionsSchema: z.ZodObject<{
|
|
156
156
|
reasoningEffort: z.ZodOptional<z.ZodEnum<{
|
|
157
|
-
none: "none";
|
|
158
157
|
low: "low";
|
|
159
158
|
medium: "medium";
|
|
160
159
|
high: "high";
|
|
160
|
+
none: "none";
|
|
161
161
|
xhigh: "xhigh";
|
|
162
162
|
}>>;
|
|
163
163
|
maxTurns: z.ZodOptional<z.ZodNumber>;
|