@cartanova/qgrid-ai-sdk 2.15.3 → 2.15.5

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
@@ -347,8 +347,8 @@ qgrid는 `grok/*` 모델을 qgrid 대시보드에 등록한 xAI 구독 토큰(
347
347
  | `qgrid(id)` | `grok/grok-4.7`(카탈로그 기본 모델), `grok/grok-4.7-build-fast`, `grok/grok-4.6`, `grok/grok-4.5`, 그 밖의 `grok/<id>` | `generateText`, `streamText` |
348
348
  | `qgrid.image(id)` | `grok/grok-imagine-image`, `grok/grok-imagine-image-2.0` | `generateImage` |
349
349
  | `qgrid.video(id)` | `grok/grok-imagine-video`, `grok/grok-imagine-video-1.5`, `grok/grok-imagine-video-1.5-lite` | `experimental_generateVideo` |
350
- | `qgrid.transcription(id)` | `grok/grok-voice-transcribe-2.0` | `transcribe` |
351
- | `qgrid.speech(id)` | `grok/grok-tts`(ID는 request log 라벨로만 쓰임) | `generateSpeech` |
350
+ | `qgrid.transcription(id)` | `grok/grok-voice-transcribe-2.0`; OpenAI 구독: `openai/gpt-4o-mini-transcribe`, `openai/gpt-4o-transcribe`, `openai/gpt-transcribe`, `openai/gpt-live-transcribe`, `openai/gpt-realtime-whisper`, `openai/whisper-1` | `transcribe` |
351
+ | `qgrid.speech(id)` | `grok/grok-tts`(ID는 request log 라벨로만 쓰임); OpenAI 구독: `openai/gpt-realtime-2.1-mini`, `openai/gpt-realtime-2.1`, `openai/gpt-realtime-1.5` | `generateSpeech` |
352
352
 
353
353
  qgrid는 카탈로그에 없는 language model ID를 거부하지 않고 그대로 전달합니다(OpenAI와 같음). 판단은
354
354
  subscription backend가 하며 다른 모델로 응답할 수 있으므로, 실제로 실행된 모델은 request log의 응답 모델(또는
@@ -488,6 +488,35 @@ speech.audio.uint8Array;
488
488
  - `transcribe`는 bytes로 오디오 형식을 감지하고 실패하면 `audio/wav`를 씁니다. 서버는 `audio/*`가 아닌 값을 거부합니다. transcript에는 segment가 없습니다(`segments: []`).
489
489
  - 음성 합성 텍스트는 최대 15,000자입니다. `voice`, `language`, `outputFormat`을 지정하지 않으면 서버 기본값 `ara`, `en`, `mp3`가 쓰이며, live 검증된 조합은 이것뿐입니다. 다른 voice, language(`@ai-sdk/xai`의 `auto` 포함), codec은 그대로 전달되지만 미검증입니다. 서버가 돌려준 오디오 MIME 타입은 `providerMetadata.qgrid.mediaType`에 있고, AI SDK의 `speech.audio.mediaType`은 bytes에서 추정한 값입니다.
490
490
 
491
+ ### OpenAI 음성 인식과 합성 (ChatGPT 구독)
492
+
493
+ `openai/` 음성 ID는 qgrid가 Codex에 쓰는 ChatGPT 구독 토큰으로 OpenAI realtime 세션을 사용합니다. 음성 인식은 Codex의 `/realtime/calls` 흐름을, 음성 합성은 텍스트를 그대로 읽는 realtime 대화 모델을 씁니다(구독에는 `gpt-4o-mini-tts`, `tts-1`이 없습니다). 옵션은 `providerOptions.openai` 아래 `@ai-sdk/openai` 이름을 따르고, `providerOptions.qgrid.tokenName`은 `openai/` 접두사가 필요합니다.
494
+
495
+ ```typescript
496
+ const transcript = await transcribe({
497
+ model: qgrid.transcription("openai/gpt-4o-mini-transcribe"),
498
+ audio: await readFile("meeting.wav"), // PCM16 WAV 또는 raw PCM만
499
+ providerOptions: { openai: { language: "en", prompt: "qgrid, Codex" } },
500
+ });
501
+
502
+ const speech = await generateSpeech({
503
+ model: qgrid.speech("openai/gpt-realtime-2.1-mini"),
504
+ text: "Your build is ready.",
505
+ voice: "marin",
506
+ outputFormat: "wav",
507
+ providerOptions: { openai: { instructions: "Calm and brisk." } },
508
+ });
509
+ ```
510
+
511
+ | 모델 | 그대로 전달(`@ai-sdk/openai` 이름) | 변환 | 미지원(경고) |
512
+ | --- | --- | --- | --- |
513
+ | Transcription | `openai.language`, `openai.prompt`(`gpt-realtime-whisper`는 HTTP 400으로 거부, `whisper-1`은 받지만 업스트림이 무시) | 입력은 PCM16 WAV(`audio/wav`) 또는 raw PCM(`audio/pcm;rate=…;channels=…`)만 가능하며 mp3/webm/ogg는 디코더가 추가되기 전까지 서버가 HTTP 400으로 거부합니다. `durationInSeconds`는 보낸 오디오에서 계산하고 `language`는 요청값을 그대로 돌려줍니다(realtime 세션은 둘 다 보고하지 않음). `segments`는 비어 있습니다 | `openai.temperature`, `openai.include`, `openai.timestampGranularities`, `openai.responseFormat`, `openai.chunkingStrategy`, `openai.streaming` |
514
+ | Speech | `voice`(기본 `marin`; Codex v2 목록 `alloy ash ballad coral echo sage shimmer verse marin cedar`, 그 외는 HTTP 400), `instructions` / `openai.instructions`(원문 그대로 읽기 지시문 뒤에 붙는 스타일 안내) | `outputFormat`은 `wav`(기본, PCM16 24 kHz) 또는 `pcm`; 그 외 값은 `@ai-sdk/openai`가 `mp3`로 되돌리듯 경고와 함께 `wav`로 바뀝니다 | `language`, `speed`, `openai.speed` |
515
+
516
+ - 음성 인식은 먼저 Codex realtime call에서 25초 예산으로 실행됩니다(call은 생성 약 30초 뒤 정리됨). 그 안에 전사가 오지 않으면 서버가 직접 realtime 전사 소켓으로 1회 재시도합니다(최대 60초). 7 MiB 인라인 제한(24 kHz PCM 약 2.5분)은 보통 첫 경로에서 몇 초 안에 끝납니다.
517
+ - 음성 합성은 TTS 모델이 아니라 텍스트를 읽는 대화 모델입니다. qgrid는 `<script>` 태그 안의 텍스트만 읽고 답하지 말라고 지시합니다. 원문 그대로 읽는지는 짧은 영어 문장과 질문형 영어 캐릭터 대사에서 검증했습니다. 모델이 실제로 말한 transcript를 요청 로그에 함께 남겨 비교할 수 있지만, 한영 혼합 텍스트에서는 transcript가 일치해도 영어 음성을 알아듣기 어려운 경우가 있었습니다.
518
+ - 모델별 동작은 서버 저장소의 `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). 2026-11-06에는 현재 프롬프트로 `gpt-realtime-2.1`과 `gpt-realtime-2.1-mini` 모두 질문형 캐릭터 대사 12/12를 그대로 읽었습니다(이전 프롬프트에서는 `gpt-realtime-2.1`이 7/12를 답변). `gpt-realtime-1.5`는 다시 측정하지 않았습니다. 정확성이 중요하면 로그의 transcript를 입력과 비교하세요.
519
+
491
520
  ## Telemetry Logger
492
521
 
493
522
  서버와 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
@@ -357,8 +357,8 @@ Grok model accepts the same factory config as the other models: `serverUrl` (def
357
357
  | `qgrid(id)` | `grok/grok-4.7` (default catalog model), `grok/grok-4.7-build-fast`, `grok/grok-4.6`, `grok/grok-4.5`, any other `grok/<id>` | `generateText`, `streamText` |
358
358
  | `qgrid.image(id)` | `grok/grok-imagine-image`, `grok/grok-imagine-image-2.0` | `generateImage` |
359
359
  | `qgrid.video(id)` | `grok/grok-imagine-video`, `grok/grok-imagine-video-1.5`, `grok/grok-imagine-video-1.5-lite` | `experimental_generateVideo` |
360
- | `qgrid.transcription(id)` | `grok/grok-voice-transcribe-2.0` | `transcribe` |
361
- | `qgrid.speech(id)` | `grok/grok-tts` (the id only labels the request log) | `generateSpeech` |
360
+ | `qgrid.transcription(id)` | `grok/grok-voice-transcribe-2.0`; OpenAI subscription: `openai/gpt-4o-mini-transcribe`, `openai/gpt-4o-transcribe`, `openai/gpt-transcribe`, `openai/gpt-live-transcribe`, `openai/gpt-realtime-whisper`, `openai/whisper-1` | `transcribe` |
361
+ | `qgrid.speech(id)` | `grok/grok-tts` (the id only labels the request log); OpenAI subscription: `openai/gpt-realtime-2.1-mini`, `openai/gpt-realtime-2.1`, `openai/gpt-realtime-1.5` | `generateSpeech` |
362
362
 
363
363
  qgrid forwards a language-model id that is not in its catalog unchanged instead of rejecting it,
364
364
  as it does for OpenAI; the subscription backend decides. It may serve a different model, so check
@@ -500,6 +500,35 @@ speech.audio.uint8Array;
500
500
  - `transcribe` detects the audio type from its bytes and falls back to `audio/wav`; the server rejects anything that is not `audio/*`. Transcripts carry no segments (`segments: []`).
501
501
  - Speech text is limited to 15,000 characters. Unset `voice`, `language` and `outputFormat` use the server defaults `ara`, `en` and `mp3`, the only combination verified live. Other voices, languages (including `@ai-sdk/xai`'s `auto`) and codecs are passed through but unverified. The server's audio MIME type is in `providerMetadata.qgrid.mediaType`; AI SDK's `speech.audio.mediaType` is sniffed from the bytes.
502
502
 
503
+ ### OpenAI transcription and speech (ChatGPT subscription)
504
+
505
+ `openai/` voice ids run on OpenAI realtime sessions with the same ChatGPT subscription tokens qgrid uses for Codex: transcription through the Codex `/realtime/calls` flow, speech through a realtime conversational model that repeats the text verbatim (there is no `gpt-4o-mini-tts` or `tts-1` on subscription). Options follow `@ai-sdk/openai` under `providerOptions.openai`; `providerOptions.qgrid.tokenName` must carry the `openai/` prefix.
506
+
507
+ ```typescript
508
+ const transcript = await transcribe({
509
+ model: qgrid.transcription("openai/gpt-4o-mini-transcribe"),
510
+ audio: await readFile("meeting.wav"), // PCM16 WAV or raw PCM only
511
+ providerOptions: { openai: { language: "en", prompt: "qgrid, Codex" } },
512
+ });
513
+
514
+ const speech = await generateSpeech({
515
+ model: qgrid.speech("openai/gpt-realtime-2.1-mini"),
516
+ text: "Your build is ready.",
517
+ voice: "marin",
518
+ outputFormat: "wav",
519
+ providerOptions: { openai: { instructions: "Calm and brisk." } },
520
+ });
521
+ ```
522
+
523
+ | Model | Faithful (`@ai-sdk/openai` names) | Transformed | Unsupported (warning) |
524
+ | --- | --- | --- | --- |
525
+ | Transcription | `openai.language`, `openai.prompt` (not on `gpt-realtime-whisper`, which rejects it with HTTP 400; accepted but ignored upstream on `whisper-1`) | input must be PCM16 WAV (`audio/wav`) or raw PCM (`audio/pcm;rate=…;channels=…`); mp3/webm/ogg are rejected by the server with HTTP 400 until a decoder lands. `durationInSeconds` is computed from the audio sent; `language` echoes the request (the realtime session reports neither); `segments` is empty | `openai.temperature`, `openai.include`, `openai.timestampGranularities`, `openai.responseFormat`, `openai.chunkingStrategy`, `openai.streaming` |
526
+ | Speech | `voice` (default `marin`; Codex v2 list `alloy ash ballad coral echo sage shimmer verse marin cedar`, others fail with HTTP 400), `instructions` / `openai.instructions` (style guidance appended to the verbatim-repeat prompt) | `outputFormat` `wav` (default, PCM16 24 kHz) or `pcm`; anything else falls back to `wav` with a warning, as `@ai-sdk/openai` falls back to `mp3` | `language`, `speed`, `openai.speed` |
527
+
528
+ - Each transcription first runs on the Codex realtime call with a 25 s budget (the call is torn down about 30 s after creation); if that window closes before the transcript arrives, the server retries once on the direct realtime transcription socket (up to 60 s). The 7 MiB inline limit (~2.5 min of 24 kHz PCM) normally completes in a few seconds on the first path.
529
+ - 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.
530
+ - 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), and `gpt-realtime-1.5` often answered the text instead of repeating it (6/13). 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); `gpt-realtime-1.5` was not re-run. Compare the logged transcript with your input when fidelity matters.
531
+
503
532
  ## Telemetry Logger
504
533
 
505
534
  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
@@ -40,12 +40,28 @@ type QgridImageModel = "openai/gpt-image-2" | "grok/grok-imagine-image" | "grok/
40
40
  /** Grok video generation model ids (xAI subscription; not yet wired to a provider factory). */
41
41
  type QgridVideoModel = "grok/grok-imagine-video" | "grok/grok-imagine-video-1.5" | "grok/grok-imagine-video-1.5-lite";
42
42
  /** Grok speech-to-text model ids. */
43
- type QgridTranscriptionModel = "grok/grok-voice-transcribe-2.0";
43
+ type QgridGrokTranscriptionModel = "grok/grok-voice-transcribe-2.0";
44
+ /**
45
+ * OpenAI subscription speech-to-text ids, served through a realtime transcription session.
46
+ * Known limits (2026-10-17 live matrix): `gpt-live-transcribe` and `gpt-realtime-whisper`
47
+ * returned about half of a 60 s clip; `gpt-realtime-whisper` rejects `openai.prompt` (HTTP 400
48
+ * before any upstream call); `whisper-1` accepts `openai.prompt` without error but the upstream
49
+ * ignores it, so it cannot steer spelling.
50
+ */
51
+ type QgridOpenAITranscriptionModel = "openai/gpt-4o-mini-transcribe" | "openai/gpt-4o-transcribe" | "openai/gpt-transcribe" | "openai/gpt-live-transcribe" | "openai/gpt-realtime-whisper" | "openai/whisper-1";
52
+ type QgridTranscriptionModel = QgridGrokTranscriptionModel | QgridOpenAITranscriptionModel;
44
53
  /**
45
54
  * Grok text-to-speech model id. Placeholder: the live probe verified the `/tts` endpoint with the
46
55
  * subscription JWT, but no TTS model id was observed (`/speech-models` returned 404).
47
56
  */
48
- type QgridSpeechModel = "grok/grok-tts";
57
+ type QgridGrokSpeechModel = "grok/grok-tts";
58
+ /**
59
+ * OpenAI subscription text-to-speech ids: realtime conversational models repeating the text
60
+ * (`gpt-4o-mini-tts`/`tts-1` are unavailable on subscription). Verbatim fidelity on 2026-10-17:
61
+ * `gpt-realtime-2.1-mini` 13/13, `gpt-realtime-2.1` 12/13, `gpt-realtime-1.5` 6/13.
62
+ */
63
+ type QgridOpenAISpeechModel = "openai/gpt-realtime-2.1-mini" | "openai/gpt-realtime-2.1" | "openai/gpt-realtime-1.5";
64
+ type QgridSpeechModel = QgridGrokSpeechModel | QgridOpenAISpeechModel;
49
65
  type QgridImageProviderConfig = QgridCommonProviderConfig;
50
66
  /** `providerOptions.qgrid` for `generateImage({ model: qgrid.image(...) })`. */
51
67
  type QgridImageProviderOptions = {
@@ -219,6 +235,12 @@ type QgridLoggerConfig = {
219
235
  export declare const QGRID_MAX_REQUEST_BODY_BYTES: number;
220
236
  /** `config` for `qgrid.image/video/transcription/speech`; same fields as the language model. */
221
237
  type QgridMediaProviderConfig = QgridImageProviderConfig;
238
+ declare const openaiQgridOptionsSchema: z.ZodObject<{
239
+ tokenName: z.ZodOptional<z.ZodString>;
240
+ logger: z.ZodOptional<z.ZodBoolean>;
241
+ }, z.core.$strip>;
242
+ /** `providerOptions.qgrid` accepted by OpenAI voice models. */
243
+ type QgridOpenAIMediaProviderOptions = z.infer<typeof openaiQgridOptionsSchema>;
222
244
  /** Non-2xx response from a qgrid media endpoint. */
223
245
  export declare class QgridMediaHttpError extends Error {
224
246
  readonly status: number;
@@ -253,6 +275,10 @@ declare const xaiImageOptionsSchema: z.ZodObject<{
253
275
  type QgridXaiImageProviderOptions = z.infer<typeof xaiImageOptionsSchema>;
254
276
  //#endregion
255
277
  //#region src/speech-model.d.ts
278
+ /** `providerOptions.openai` honored by `qgrid.speech("openai/...")`. */
279
+ type QgridOpenAISpeechProviderOptions = {
280
+ instructions?: string;
281
+ };
256
282
  /**
257
283
  * Grok text-to-speech through `POST /api/qgrid/speak`. `voice`, `language` and `outputFormat`
258
284
  * map to the server's `voice`, `language` and `format`; when omitted the server applies
@@ -265,6 +291,18 @@ declare function createQgridSpeech(modelId: QgridSpeechModel, config?: QgridMedi
265
291
  type QgridXaiTranscriptionProviderOptions = {
266
292
  language?: string;
267
293
  };
294
+ /**
295
+ * `providerOptions.openai` honored by `qgrid.transcription("openai/...")`.
296
+ *
297
+ * `prompt` steers vocabulary on `gpt-4o-mini-transcribe`, `gpt-4o-transcribe`, `gpt-transcribe`
298
+ * and `gpt-live-transcribe`. `openai/gpt-realtime-whisper` rejects it (the server answers HTTP
299
+ * 400 before any upstream call). `openai/whisper-1` accepts it but the upstream ignores it
300
+ * (verified 2026-10-17: "qgrid" still came back as "Thread"), so do not rely on it for spelling.
301
+ */
302
+ type QgridOpenAITranscriptionProviderOptions = {
303
+ language?: string;
304
+ prompt?: string;
305
+ };
268
306
  declare function createQgridTranscription(modelId: QgridTranscriptionModel, config?: QgridMediaProviderConfig): TranscriptionModelV3;
269
307
  //#endregion
270
308
  //#region src/video-model.d.ts
@@ -337,4 +375,4 @@ export declare namespace qgrid {
337
375
  var speech: typeof createQgridSpeech;
338
376
  }
339
377
  //#endregion
340
- export { type QgridAnthropicEffort, type QgridAnthropicModel, type QgridAnthropicProviderConfig, type QgridAnthropicProviderOptions, type QgridGrokEffort, type QgridGrokMediaProviderOptions, type QgridGrokModel, type QgridImageGenerationMetadata, type QgridImageModel, type QgridImageProviderConfig, type QgridImageProviderOptions, type QgridLoggerConfig, type QgridMediaProviderConfig, type QgridOpenAIEffort, type QgridOpenAIModel, type QgridOpenAIProviderConfig, type QgridOpenAIProviderOptions, type QgridProviderConfig, type QgridProviderOptions, type QgridSpeechModel, type QgridSupportedModel, type QgridTranscriptionModel, type QgridVideoModel, type QgridVideoPollOptions, type QgridVideoResult, type QgridXaiImageProviderOptions, type QgridXaiProviderOptions, type QgridXaiTranscriptionProviderOptions, type QgridXaiVideoProviderOptions, qgrid as default };
378
+ export { type QgridAnthropicEffort, type QgridAnthropicModel, type QgridAnthropicProviderConfig, type QgridAnthropicProviderOptions, type QgridGrokEffort, type QgridGrokMediaProviderOptions, type QgridGrokModel, type QgridGrokSpeechModel, type QgridGrokTranscriptionModel, type QgridImageGenerationMetadata, type QgridImageModel, type QgridImageProviderConfig, type QgridImageProviderOptions, type QgridLoggerConfig, type QgridMediaProviderConfig, type QgridOpenAIEffort, type QgridOpenAIMediaProviderOptions, type QgridOpenAIModel, type QgridOpenAIProviderConfig, type QgridOpenAIProviderOptions, type QgridOpenAISpeechModel, type QgridOpenAISpeechProviderOptions, type QgridOpenAITranscriptionModel, type QgridOpenAITranscriptionProviderOptions, type QgridProviderConfig, type QgridProviderOptions, type QgridSpeechModel, type QgridSupportedModel, type QgridTranscriptionModel, type QgridVideoModel, type QgridVideoPollOptions, type QgridVideoResult, type QgridXaiImageProviderOptions, type QgridXaiProviderOptions, type QgridXaiTranscriptionProviderOptions, type QgridXaiVideoProviderOptions, qgrid as default };
package/dist/index.js CHANGED
@@ -1000,6 +1000,27 @@ const GROK_MODEL_ID = /^grok\/[A-Za-z0-9._-]+$/;
1000
1000
  function assertGrokModelId(modelId, label) {
1001
1001
  if (!GROK_MODEL_ID.test(modelId)) throw new Error(`${label}: unsupported model ${modelId}; expected grok/<id>`);
1002
1002
  }
1003
+ /** Voice factories accept `grok/<id>` and `openai/<id>`; everything else throws at creation. */
1004
+ function voiceProviderOf(modelId, label) {
1005
+ if (GROK_MODEL_ID.test(modelId)) return "grok";
1006
+ if (/^openai\/[A-Za-z0-9._-]+$/.test(modelId)) return "openai";
1007
+ throw new Error(`${label}: unsupported model ${modelId}; expected grok/<id> or openai/<id>`);
1008
+ }
1009
+ const openaiQgridOptionsSchema = z.object({
1010
+ tokenName: z.string().refine((value) => /^openai\/.+/.test(value.trim()), { message: "tokenName must be a non-empty OpenAI token name with the openai/ prefix" }).optional(),
1011
+ logger: z.boolean({ message: "logger must be a boolean" }).optional()
1012
+ });
1013
+ /** `providerOptions.qgrid` for an `openai/` voice model: tokenName must carry the openai/ prefix. */
1014
+ function parseOpenAIQgridOptions(raw, label) {
1015
+ if (raw === void 0) return {};
1016
+ if (!providerOptionsObjectSchema.safeParse(raw).success) throw new Error(`${label}: providerOptions.qgrid must be an object`);
1017
+ const parsed = openaiQgridOptionsSchema.safeParse(raw);
1018
+ if (!parsed.success) throw new Error(`${label}: ${parsed.error.issues.map((issue) => issue.message).join("; ")}`);
1019
+ const request = {};
1020
+ if (parsed.data.tokenName !== void 0) request.tokenName = parsed.data.tokenName;
1021
+ if (parsed.data.logger === false) request.logger = false;
1022
+ return request;
1023
+ }
1003
1024
  function formatMiB(bytes) {
1004
1025
  return `${(bytes / 1024 / 1024).toFixed(2)} MiB`;
1005
1026
  }
@@ -1118,21 +1139,29 @@ function parseGrokQgridOptions(raw, label) {
1118
1139
  * `honored`. Malformed values throw; documented-but-unsupported keys become warnings.
1119
1140
  */
1120
1141
  function parseXaiMediaOptions(raw, schema, honored, label) {
1142
+ return parseNamespacedMediaOptions("xai", raw, schema, honored, label);
1143
+ }
1144
+ /**
1145
+ * Validates `providerOptions.<namespace>` against the official provider's schema and warns for
1146
+ * every set key qgrid does not honor, so code written for `@ai-sdk/xai` or `@ai-sdk/openai`
1147
+ * keeps parsing and learns what was dropped.
1148
+ */
1149
+ function parseNamespacedMediaOptions(namespace, raw, schema, honored, label) {
1121
1150
  if (raw === void 0) return {
1122
1151
  options: void 0,
1123
1152
  warnings: []
1124
1153
  };
1125
1154
  const record = providerOptionsObjectSchema.safeParse(raw);
1126
- if (!record.success) throw new Error(`${label}: providerOptions.xai must be an object`);
1155
+ if (!record.success) throw new Error(`${label}: providerOptions.${namespace} must be an object`);
1127
1156
  const parsed = schema.safeParse(raw);
1128
- if (!parsed.success) throw new Error(`${label}: invalid providerOptions.xai: ${formatIssues(parsed.error)}`);
1157
+ if (!parsed.success) throw new Error(`${label}: invalid providerOptions.${namespace}: ${formatIssues(parsed.error)}`);
1129
1158
  const warnings = [];
1130
1159
  for (const [key, value] of Object.entries(record.data)) {
1131
1160
  if (value === void 0 || value === null || honored.has(key)) continue;
1132
1161
  warnings.push({
1133
1162
  type: "unsupported",
1134
- feature: `providerOptions.xai.${key}`,
1135
- details: `qgrid does not support providerOptions.xai.${key} for this model; it was ignored.`
1163
+ feature: `providerOptions.${namespace}.${key}`,
1164
+ details: `qgrid does not support providerOptions.${namespace}.${key} for this model; it was ignored.`
1136
1165
  });
1137
1166
  }
1138
1167
  return {
@@ -1476,6 +1505,21 @@ const xaiSpeechOptionsSchema = z.object({
1476
1505
  withTimestamps: z.boolean().nullish(),
1477
1506
  replace: z.record(z.string(), z.string()).nullish()
1478
1507
  });
1508
+ /**
1509
+ * `providerOptions.openai` for OpenAI speech, field-for-field from `@ai-sdk/openai`
1510
+ * `openaiSpeechModelOptionsSchema` (vercel/ai fce9210). `instructions` is honored as style
1511
+ * guidance for the realtime model; `speed` warns.
1512
+ */
1513
+ const openaiSpeechOptionsSchema = z.object({
1514
+ instructions: z.string().nullish(),
1515
+ speed: z.number().min(.25).max(4).nullish()
1516
+ });
1517
+ const OPENAI_SPEECH_HONORED = /* @__PURE__ */ new Set(["instructions"]);
1518
+ /** Realtime speech output: PCM16 24 kHz, as WAV (default) or raw PCM. */
1519
+ const OPENAI_SPEECH_FORMATS = ["wav", "pcm"];
1520
+ function isOpenAISpeechFormat(value) {
1521
+ return OPENAI_SPEECH_FORMATS.some((format) => format === value);
1522
+ }
1479
1523
  const speakOutputSchema = z.object({
1480
1524
  audio: z.string(),
1481
1525
  mimeType: z.string(),
@@ -1495,8 +1539,9 @@ function base64ToBytes(data) {
1495
1539
  * `ara` / `en` / `mp3` (the only combination verified live). The model id only labels the log.
1496
1540
  */
1497
1541
  function createQgridSpeech(modelId, config) {
1498
- assertGrokModelId(modelId, LABEL$2);
1542
+ const provider = voiceProviderOf(modelId, LABEL$2);
1499
1543
  const { serverUrl, projectName } = resolveMediaConfig(config);
1544
+ if (provider === "openai") return createOpenAISpeech(modelId, serverUrl, projectName);
1500
1545
  return {
1501
1546
  specificationVersion: "v3",
1502
1547
  provider: QGRID_PROVIDER_NAME,
@@ -1570,6 +1615,89 @@ function createQgridSpeech(modelId, config) {
1570
1615
  }
1571
1616
  };
1572
1617
  }
1618
+ /**
1619
+ * OpenAI subscription speech: a realtime conversational model repeating the text verbatim (there
1620
+ * is no `gpt-4o-mini-tts` on subscription). Follows `@ai-sdk/openai` option names: `voice`
1621
+ * (default `marin`, Codex v2 list), `outputFormat` `wav` (default) or `pcm` — other formats
1622
+ * warn and fall back to wav, as the official provider falls back to mp3 — `openai.instructions`
1623
+ * as style guidance, `openai.speed` and `language` unsupported with the official warning text.
1624
+ */
1625
+ function createOpenAISpeech(modelId, serverUrl, projectName) {
1626
+ return {
1627
+ specificationVersion: "v3",
1628
+ provider: QGRID_PROVIDER_NAME,
1629
+ modelId,
1630
+ async doGenerate(options) {
1631
+ if (!options.text.trim()) throw new Error(`${LABEL$2}: text must not be empty`);
1632
+ if (options.text.length > MAX_TEXT_CHARS) throw new Error(`${LABEL$2}: text is limited to ${MAX_TEXT_CHARS} characters`);
1633
+ const qgridOptions = parseOpenAIQgridOptions(options.providerOptions?.qgrid, LABEL$2);
1634
+ const openai = parseNamespacedMediaOptions("openai", options.providerOptions?.openai, openaiSpeechOptionsSchema, OPENAI_SPEECH_HONORED, LABEL$2);
1635
+ const warnings = [];
1636
+ let format;
1637
+ if (options.outputFormat !== void 0) {
1638
+ if (isOpenAISpeechFormat(options.outputFormat)) format = options.outputFormat;
1639
+ else {
1640
+ format = "wav";
1641
+ warnings.push({
1642
+ type: "unsupported",
1643
+ feature: "outputFormat",
1644
+ details: `Unsupported output format: ${options.outputFormat}. Using wav instead.`
1645
+ });
1646
+ }
1647
+ }
1648
+ if (options.language !== void 0) warnings.push({
1649
+ type: "unsupported",
1650
+ feature: "language",
1651
+ details: `OpenAI speech models do not support language selection. Language parameter "${options.language}" was ignored.`
1652
+ });
1653
+ if (options.speed !== void 0) warnings.push({
1654
+ type: "unsupported",
1655
+ feature: "speed"
1656
+ });
1657
+ warnings.push(...openai.warnings);
1658
+ const requestParams = {
1659
+ model: modelId,
1660
+ text: options.text
1661
+ };
1662
+ if (options.voice !== void 0) requestParams.voice = options.voice;
1663
+ if (format !== void 0) requestParams.format = format;
1664
+ const instructions = options.instructions ?? openai.options?.instructions ?? void 0;
1665
+ if (instructions !== void 0) requestParams.instructions = instructions;
1666
+ if (projectName) requestParams.projectName = projectName;
1667
+ const params = {
1668
+ ...requestParams,
1669
+ ...qgridOptions
1670
+ };
1671
+ const body = serializeMediaBody(params, LABEL$2);
1672
+ const timestamp = /* @__PURE__ */ new Date();
1673
+ const { data, headers } = await postMediaEndpoint({
1674
+ serverUrl,
1675
+ endpoint: "speak",
1676
+ body,
1677
+ schema: speakOutputSchema,
1678
+ label: LABEL$2,
1679
+ signal: options.abortSignal,
1680
+ headers: options.headers
1681
+ });
1682
+ const qgridMetadata = {
1683
+ mediaType: data.mimeType,
1684
+ tokenName: data.tokenName
1685
+ };
1686
+ if (data.requestLogId !== void 0) qgridMetadata.requestLogId = data.requestLogId;
1687
+ return {
1688
+ audio: base64ToBytes(data.audio),
1689
+ warnings,
1690
+ request: { body: params },
1691
+ response: {
1692
+ timestamp,
1693
+ modelId,
1694
+ headers
1695
+ },
1696
+ providerMetadata: { qgrid: qgridMetadata }
1697
+ };
1698
+ }
1699
+ };
1700
+ }
1573
1701
  //#endregion
1574
1702
  //#region src/transcription-model.ts
1575
1703
  const LABEL$1 = "qgrid.transcription";
@@ -1604,6 +1732,40 @@ const xaiTranscriptionOptionsSchema = z.object({
1604
1732
  streaming: z.object({}).loose().optional()
1605
1733
  });
1606
1734
  const XAI_TRANSCRIPTION_HONORED = /* @__PURE__ */ new Set(["language"]);
1735
+ /**
1736
+ * `providerOptions.openai` for OpenAI transcription, field-for-field from `@ai-sdk/openai`
1737
+ * `openAITranscriptionModelOptions` (vercel/ai fce9210). The realtime transcription session
1738
+ * honors `language` and `prompt`; the rest warn.
1739
+ */
1740
+ const openaiTranscriptionOptionsSchema = z.object({
1741
+ include: z.array(z.string()).optional(),
1742
+ language: z.string().optional(),
1743
+ prompt: z.string().optional(),
1744
+ temperature: z.number().min(0).max(1).optional(),
1745
+ timestampGranularities: z.array(z.enum(["word", "segment"])).optional(),
1746
+ responseFormat: z.enum([
1747
+ "json",
1748
+ "verbose_json",
1749
+ "diarized_json"
1750
+ ]).optional(),
1751
+ chunkingStrategy: z.union([z.literal("auto"), z.object({
1752
+ type: z.literal("server_vad"),
1753
+ threshold: z.number().min(0).max(1).optional(),
1754
+ prefixPaddingMs: z.number().int().min(0).optional(),
1755
+ silenceDurationMs: z.number().int().min(0).optional()
1756
+ })]).optional(),
1757
+ streaming: z.object({
1758
+ delay: z.enum([
1759
+ "minimal",
1760
+ "low",
1761
+ "medium",
1762
+ "high",
1763
+ "xhigh"
1764
+ ]).optional(),
1765
+ include: z.array(z.string()).optional()
1766
+ }).optional()
1767
+ });
1768
+ const OPENAI_TRANSCRIPTION_HONORED = /* @__PURE__ */ new Set(["language", "prompt"]);
1607
1769
  const transcribeOutputSchema = z.object({
1608
1770
  text: z.string(),
1609
1771
  language: z.string().optional(),
@@ -1618,7 +1780,7 @@ function normalizeMediaType(mediaType) {
1618
1780
  return bare;
1619
1781
  }
1620
1782
  function createQgridTranscription(modelId, config) {
1621
- assertGrokModelId(modelId, LABEL$1);
1783
+ const provider = voiceProviderOf(modelId, LABEL$1);
1622
1784
  const { serverUrl, projectName } = resolveMediaConfig(config);
1623
1785
  return {
1624
1786
  specificationVersion: "v3",
@@ -1629,16 +1791,24 @@ function createQgridTranscription(modelId, config) {
1629
1791
  const audio = options.audio instanceof Uint8Array ? bytesToBase64(options.audio) : options.audio;
1630
1792
  if (audio.length === 0) throw new Error(`${LABEL$1}: audio is empty`);
1631
1793
  assertInlineMediaFits([audio], LABEL$1);
1632
- const qgridOptions = parseGrokQgridOptions(options.providerOptions?.qgrid, LABEL$1);
1633
- const xai = parseXaiMediaOptions(options.providerOptions?.xai, xaiTranscriptionOptionsSchema, XAI_TRANSCRIPTION_HONORED, LABEL$1);
1634
- const warnings = [...xai.warnings];
1635
- const language = xai.options?.language ?? void 0;
1794
+ const qgridOptions = provider === "openai" ? parseOpenAIQgridOptions(options.providerOptions?.qgrid, LABEL$1) : parseGrokQgridOptions(options.providerOptions?.qgrid, LABEL$1);
1795
+ const warnings = [];
1636
1796
  const params = {
1637
1797
  model: modelId,
1638
1798
  audio,
1639
1799
  mediaType
1640
1800
  };
1641
- if (language !== void 0) params.language = language;
1801
+ if (provider === "openai") {
1802
+ const openai = parseNamespacedMediaOptions("openai", options.providerOptions?.openai, openaiTranscriptionOptionsSchema, OPENAI_TRANSCRIPTION_HONORED, LABEL$1);
1803
+ warnings.push(...openai.warnings);
1804
+ if (openai.options?.language !== void 0) params.language = openai.options.language;
1805
+ if (openai.options?.prompt !== void 0) params.prompt = openai.options.prompt;
1806
+ } else {
1807
+ const xai = parseXaiMediaOptions(options.providerOptions?.xai, xaiTranscriptionOptionsSchema, XAI_TRANSCRIPTION_HONORED, LABEL$1);
1808
+ warnings.push(...xai.warnings);
1809
+ const language = xai.options?.language ?? void 0;
1810
+ if (language !== void 0) params.language = language;
1811
+ }
1642
1812
  if (projectName) params.projectName = projectName;
1643
1813
  const body = serializeMediaBody({
1644
1814
  ...params,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartanova/qgrid-ai-sdk",
3
- "version": "2.15.3",
3
+ "version": "2.15.5",
4
4
  "description": "AI SDK LanguageModelV3 and ImageModelV3 provider for qgrid",
5
5
  "repository": {
6
6
  "type": "git",