Deuz SDK 2.0 çıktı — store’lar, guardrail’ler, handoff ve sıfır yapılandırmalı MCP. 2.0’da neler yeni
Deuz SDK
Modules

Transcription (STT)

transcribe — audio in, text (and timings) out, over the OpenAI and Deepgram speech-to-text wires.

Doküman sayfalarının içeriği İngilizce’dir. Gezinme, arama ve arayüz metinleri seçtiğiniz dile göre çevrilir.

transcribe is the other half of generateSpeech: audio goes in, text comes out. Like every media module it is one request and one answer — no streaming, no loop.

Two wires ship in 2.0, and they are far less alike than the two TTS wires:

ProviderFactoryWire
OpenAI (and OpenAI-compatible relays)createOpenAITranscriptionPOST {baseURL}/audio/transcriptions, multipart/form-data, Authorization: Bearer
DeepgramcreateDeepgramPOST {baseURL}/listen?model=…, raw audio as the body, Authorization: Token

Transcription models are a separate kind from chat LanguageModel (surface: 'openai-transcription' | 'deepgram-transcription') — they cannot be passed to streamChat or generateText. The module is pure and edge-safe: HTTP goes through deps.fetch and the API key is resolved from the factory, createClient, or a deps.keyProvider, never from the environment.

Audio input to a chat model is a file part

This page is about a dedicated STT endpoint. Audio you want a chat model to hear travels as filePart({ mediaType: 'audio/…' }), exactly like a PDF or an image — the Part union stayed at five members in 2.0.

Verified against mocks, not yet against a live API

Everything on this page is pinned by unit tests (test/transcription.test.ts) that drive both adapters through an injected deps.fetch and assert the exact URL, query string, auth scheme, multipart fields, and response parsing. That proves the SDK sends and reads what this page says it does. It does not prove OpenAI and Deepgram accept it — this module has no live-API test; test/live/ covers DeepSeek, Google, and xAI only.

The wire details below come from the providers' documentation. Run one real file through it before you ship, and correct anything that turns out wrong with providerOptions rather than waiting for an SDK release.

Choosing a wire

The two providers are not interchangeable, and the differences show up in the first hour rather than the second month:

If you…PickBecause
Already have an OpenAI key and want the shortest pathcreateOpenAITranscriptionOne key, one factory, and gpt-4o-transcribe needs no tuning.
Need word timingscreateDeepgram, or OpenAI whisper-1The gpt-4o-* models cannot return them at all — see the verbose_json gate.
Have the audio in object storage alreadycreateDeepgram with audio: { url }Deepgram fetches it itself; nothing large moves through your process.
Need per-word confidence, or diarizationcreateDeepgramconfidence comes back per word; diarize rides providerOptions.
Want a vocabulary hint (names, jargon)createOpenAITranscriptionprompt is OpenAI-only. Deepgram's equivalent is keyterm via providerOptions.
Are transcribing to feed a chat model, not a humanEitherThe extra fields cost nothing if you ignore them.

And when not to use this module at all: if you want a chat model to reason about how something was said (tone, background, overlapping speakers), a transcript throws that away — send the audio to a model whose registry row sets audio: true as a filePart instead. If you want live captions, see what this does not do: there is no streaming path here.

transcribe

Import transcribe from @deuz-sdk/core/transcription. Build a model descriptor with a factory, hand it the audio bytes, and pass the mediaType along with them.

transcribe.ts
import { transcribe, createOpenAITranscription } from '@deuz-sdk/core/transcription';
import { readFile } from 'node:fs/promises';

const openaiStt = createOpenAITranscription({
  apiKey: process.env.OPENAI_API_KEY!,
});

const { text, usage } = await transcribe({
  model: openaiStt('gpt-4o-transcribe'),
  audio: await readFile('meeting.mp3'),
  mediaType: 'audio/mpeg',
  language: 'tr',
});

console.log(text);              // 'Merhaba dünya.'
console.log(usage.inputTokens); // token-billed on this model

Options

transcribe(options) takes a single TranscribeOptions object.

OptionTypeDefaultNotes
modelTranscriptionModelFrom createOpenAITranscription / createDeepgram. Required.
audioUint8Array | ArrayBuffer | Blob | { url }Required. { url } is Deepgram-only — see below.
mediaTypestringfrom a Blob's own typee.g. 'audio/mpeg'. Strongly recommended — see Media types and filenames.
filenamestringderived from mediaTypeOverrides the multipart filename (OpenAI only).
languagestringprovider auto-detectISO-639-1 hint, e.g. 'tr'.
promptstringunsetVocabulary/style hint (OpenAI only).
timestampsbooleanfalseAsk for segments + words — see Timestamps.
providerOptions{ openai?, deepgram? }unsetExtra form fields / query params. Canonical fields always win.
signalAbortSignalunsetAborts the underlying fetch.
headersRecord<string, string>unsetPer-call headers, merged over factory headers.
depsDependenciesresolved defaultsInject fetch, keyProvider, observer, …

Result shape

interface TranscribeResult {
  text: string;
  language?: string;        // detected or echoed, when the provider reports one
  durationSeconds?: number; // audio length, when the provider reports one
  segments?: TranscriptionSegment[]; // { start, end, text }
  words?: TranscriptionWord[];       // { word, start, end, confidence? }
  usage: TranscriptionUsage;
  raw: unknown;             // the untouched provider JSON
}

Every optional field is absent, not undefined, when the wire did not produce it — a plain json OpenAI response yields exactly { text, usage, raw }. raw always carries the provider's own JSON, so a field this module does not model is never lost.

Timestamps

timestamps: true asks for per-sentence segments and per-word words. What that costs — and whether it is even legal — depends entirely on the wire.

Modelresponse_format sentTimings returned
whisper-1 (OpenAI), timestamps: trueverbose_json + timestamp_granularities[] = segment, wordsegments and words
whisper-1 (OpenAI), timestamps unsetjsonnone
gpt-4o-transcribe, gpt-4o-mini-transcribejsonalwaysnone
Deepgram (any model)n/awords always; segments whenever smart_format is on

verbose_json is whisper-only

verbose_json is the only OpenAI response format that carries timings, and the gpt-4o-transcribe family rejects it with a 400. So timestamps: true is gated on the model slug: it upgrades the format only for whisper-*, and silently stays on json for a 4o model rather than failing the call. If you need word timings from OpenAI, use whisper-1.

timestamps.ts
const { segments, words } = await transcribe({
  model: openaiStt('whisper-1'),
  audio: bytes,
  mediaType: 'audio/mpeg',
  timestamps: true,
});

segments?.[0]; // { start: 0, end: 4.2, text: ' Merhaba dünya.' }
words?.[0];    // { word: 'Merhaba', start: 0, end: 0.8 }

Media types and filenames

OpenAI does not read the multipart part's Content-Type — it dispatches on the file extension. So the upload filename is load-bearing, and this module derives it from mediaType:

mediaTypeFilename sent
audio/mpeg, audio/mp3, audio/mpgaaudio.mp3
audio/wav, audio/x-wav, audio/waveaudio.wav
audio/mp4 / audio/m4a, audio/x-m4aaudio.mp4 / audio.m4a
audio/webm, audio/ogg, audio/flac, audio/aacaudio.webm, audio.ogg, audio.flac, audio.aac
anything else, or omittedaudio.bin — OpenAI will reject it

Parameters are ignored (audio/wav; codecs=1 is still audio.wav), a Blob's own type fills in when mediaType is omitted, and filename overrides the derivation outright. On the Deepgram wire the same value is sent as the request Content-Type instead, falling back to application/octet-stream.

Never set content-type yourself on the OpenAI wire

The multipart request deliberately carries no content-type header from this SDK: fetch writes multipart/form-data; boundary=… when it sees a FormData body, and a hand-written header would omit the boundary and 400 every request. Per-call headers are still merged, so do not add one there either.

Deepgram

Deepgram is not OpenAI-shaped. Three differences are worth internalizing.

1. Authorization: Token, not Bearer. Deepgram uses its own scheme; a bearer token is a 401. The adapter writes it for you:

deepgram.ts
import { transcribe, createDeepgram } from '@deuz-sdk/core/transcription';

const deepgram = createDeepgram({ apiKey: process.env.DEEPGRAM_API_KEY! });

const { text, words, durationSeconds } = await transcribe({
  model: deepgram('nova-3'),
  audio: bytes,
  mediaType: 'audio/mpeg',
  language: 'en',
});
// POST https://api.deepgram.com/v1/listen?model=nova-3&language=en&smart_format=true
// Authorization: Token <key>          ← not Bearer
// Content-Type: audio/mpeg            ← the audio IS the body

2. The audio is the request body, and a URL works too. Bytes are POSTed raw — no multipart wrapper. Deepgram can also fetch the audio itself, which is the cheapest path for anything already sitting in object storage:

deepgram-url.ts
const { text } = await transcribe({
  model: deepgram('nova-3'),
  audio: { url: 'https://cdn.example/talk.mp3' },
});
// body → {"url":"https://cdn.example/talk.mp3"}, content-type: application/json

Passing { url } to an OpenAI model throws an InvalidRequestError before any request is made — that wire uploads bytes, full stop.

3. smart_format is on by default. It adds punctuation and casing, and it is what produces the paragraphs block that this module maps into segments. Turn it off through providerOptions if you want the raw token stream:

providerOptions: { deepgram: { smart_format: false } } // → the param is simply omitted

Word entries prefer Deepgram's punctuated_word over the raw word when smart formatting produced one, and carry Deepgram's per-word confidence. language comes from results.channels[0].detected_language, durationSeconds from metadata.duration.

A real pipeline: recording → notes

Transcription is rarely the deliverable; it is the step before one. The whole point of segments is that they survive into the summary as citations you can jump to.

meeting-notes.ts
import { generateText } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';
import { transcribe, createDeepgram } from '@deuz-sdk/core/transcription';

const deepgram = createDeepgram({ apiKey: process.env.DEEPGRAM_API_KEY! });
const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

export async function meetingNotes(recordingUrl: string) {
  // Deepgram pulls the file itself: a 90-minute recording never touches this
  // process, and `diarize` labels who spoke — a Deepgram query param, not a
  // canonical option, so it rides providerOptions.
  const { text, segments, durationSeconds, usage } = await transcribe({
    model: deepgram('nova-3'),
    audio: { url: recordingUrl },
    language: 'en',
    providerOptions: { deepgram: { diarize: true, utterances: true } },
  });

  // Timestamped lines, so the summary can cite a position in the audio.
  const timeline = (segments ?? [])
    .map((s) => `[${Math.floor(s.start)}s] ${s.text}`)
    .join('\n');

  const { text: notes } = await generateText({
    model: anthropic('claude-sonnet-4-6'),
    instructions:
      'Summarize this meeting transcript into Decisions, Action items (with owners), ' +
      'and Open questions. Cite the [Ns] marker for every claim.',
    prompt: timeline || text,
    maxOutputTokens: 2_000,
  });

  return { notes, durationSeconds, billedSeconds: usage.seconds };
}

Notes on the shape, all of them things that bite once:

  • segments is optional and this code treats it as such. On Deepgram it exists only when smart_format produced paragraphs (it is on by default); on OpenAI it exists only for whisper-1 with timestamps: true. segments ?? [] with a fall back to plain text is not defensive noise — it is the actual contract.
  • Diarization does not reach the canonical shape. TranscriptionSegment is { start, end, text } — no speaker field. Deepgram's speaker labels are in raw, which is exactly what raw is for: read result.raw when you need a field this module does not model.
  • A long recording is a long wait. There is no streaming and no progress signal; the promise resolves when the provider is done. Put this behind a queue, not an HTTP request handler.
  • The transcript is untrusted input. It came from a microphone someone else controlled. If it flows into a tool-calling agent rather than a summary, treat it the way you would a user message — see request validation and guardrails.

usage — two meters, one shape

Providers do not agree on what they charge for, so TranscriptionUsage carries every field optionally and fills in only what the response actually reported:

interface TranscriptionUsage {
  seconds?: number;      // audio duration billing
  inputTokens?: number;
  outputTokens?: number;
  audioTokens?: number;  // the audio-derived slice of inputTokens
  totalTokens?: number;
}
Wire / modelusage you get
gpt-4o-transcribe, gpt-4o-mini-transcribe{ inputTokens, outputTokens, audioTokens, totalTokens }
whisper-1 with verbose_json{ seconds } — taken from the response duration
whisper-1 with a { type: 'duration' } usage block{ seconds }
Deepgram{ seconds } — from metadata.duration

Read usage.seconds ?? usage.totalTokens if you need one number for a meter, and branch on which one is present if you are pricing the call.

Two honest caveats about that number. Nothing here converts it to money: deps.priceProvider is never consulted by transcribe, PRICES_2026 has no STT rows, and no cost event reaches the observation stream. And usage can be empty: every field is optional, so a relay that omits both meters yields {} rather than a zero — usage.seconds ?? usage.totalTokens ?? 0 is the safe read, and durationSeconds is a second source for the same fact when the provider reports it.

Practically: seconds scales with the recording, inputTokens scales with the recording and the model's tokenizer, and neither scales with the length of the transcript. A silent hour costs the same as a talkative one.

providerOptions

providerOptions is keyed by provider name and is a shallow escape hatch for wire fields the SDK does not model. Canonical fields always win — the hatch may add, never redefine:

await transcribe({
  model: openaiStt('whisper-1'),
  audio: bytes,
  mediaType: 'audio/mpeg',
  language: 'tr',
  providerOptions: {
    openai: { temperature: 0, chunking_strategy: 'auto', language: 'de' },
  },
});
// form fields → file, model, language=tr (NOT 'de'), response_format=json,
//               temperature=0, chunking_strategy=auto

Values are coerced with String(value); undefined / null entries are dropped. On the OpenAI wire the entries become extra multipart fields; on Deepgram they become extra query parameters (diarize, utterances, detect_language, keyterm, …), with smart_format special-cased as described above.

Creating transcription model descriptors

Both factories return a TranscriptionProvider — a (modelId: string) => TranscriptionModel function. Factory settings are carried on a private symbol, so the public TranscriptionModel shape stays clean and the key never leaks via enumeration.

SettingTypeDefaultNotes
apiKeystringResolved against keyProvider / createClient if omitted.
baseURLstringper-wire (below)Point at any compatible relay.
fetchtypeof fetchdeps.fetchFactory fetch wins over deps.fetch.
headersRecord<string, string>Default headers for every call.
providerstring'openai' / 'deepgram'Logical id used for key/baseURL resolution.
FactoryDefault baseURLTypical models
createOpenAITranscriptionhttps://api.openai.com/v1whisper-1, gpt-4o-transcribe, gpt-4o-mini-transcribe
createDeepgramhttps://api.deepgram.com/v1nova-3, nova-2, whisper-large

Key resolution

The same G1 precedence chain as every other module: deps.keyProvider (highest) → factory apiKeycreateClient's apiKeys[provider] (lowest). When none of them produce a key, the call throws an AuthenticationError before any network request is made.

createClient.ts
import { createClient } from '@deuz-sdk/core';

const client = createClient({
  apiKeys: { openai: process.env.OPENAI_API_KEY!, deepgram: process.env.DEEPGRAM_API_KEY! },
});

baseUrls[provider] works the same way for the base URL, sitting between the factory setting and the per-wire default.

Observability

A transcription call emits the auxiliary operation.started / operation.completed / operation.failed events under the 'transcription' subsystem with operation: 'transcription.transcribe' — the same shape image, speech, and embedding use. Without an observer the fast path applies: no events are built at all. See Observability.

Errors

The canonical status→class table, shared with every media module: 401/403AuthenticationError, 404ModelNotFoundError, 429RateLimitError (with Retry-After parsed into retryAfterMs), 529OverloadedError, other 4xxInvalidRequestError, 5xx → a retryable APICallError.

Deepgram wraps its failures in { err_code, err_msg } instead of OpenAI's { error: { message } }; the envelope is normalized before mapping, so err_msg lands on error.message and err_code on error.upstreamType.

Two failures happen before the request is ever sent: an unsupported audio shape, and { url } on the OpenAI wire — both InvalidRequestError. A missing API key is the third, from the G1 chain. Failed bodies are read once and parsed as JSON when they can be (a proxy's HTML 502 becomes the message instead of a second exception), and an x-request-id response header lands on error.requestId.

What transcribe does not do

  • No streaming, no partial results. Deepgram and OpenAI both offer live/websocket transcription; this module speaks only the batch endpoints. Live captions are out of scope in 2.0.
  • No chunking, and no size check. Providers cap how much audio one request may carry, and the SDK neither measures your bytes nor splits them — an over-long file comes back as an InvalidRequestError from the provider. Segment it yourself, or hand Deepgram a { url } so the bytes never pass through you.
  • No retries, no breaker, no timeout. One plain fetch, like every media module. maxRetries does not exist here; retryAfterMs on the error is advice you act on. Pass signal for a deadline.
  • No format conversion or resampling. Whatever bytes you pass are the bytes uploaded. The only transformation is the filename derivation, which exists because OpenAI dispatches on the extension.
  • No speaker labels in the canonical shape. TranscriptionSegment has no speaker; Deepgram diarization output lives in raw.
  • No capability matrix. TranscriptionModel is not a LanguageModel, so getModelCapabilities does not apply and an unknown slug produces no warning — the provider's 404ModelNotFoundError is the only signal.
  • Only two wires. A third STT vendor needs a new TranscriptionAdapter; today's extension point is pointing baseURL at a relay that mimics one of these two.

Bu sayfada