🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@ai-sdk/google

Package Overview
Dependencies
Maintainers
3
Versions
602
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ai-sdk/google - npm Package Compare versions

Comparing version
4.0.36
to
4.0.37
+26
src/speech-transla...gle-speech-translation-model-options.ts
import {
lazySchema,
zodSchema,
type InferSchema,
} from '@ai-sdk/provider-utils';
import { z } from 'zod/v4';
export type GoogleSpeechTranslationModelId =
| 'gemini-3.5-live-translate-preview'
| (string & {});
export const googleSpeechTranslationModelOptions = lazySchema(() =>
zodSchema(
z.object({
/**
* Whether input audio already in the target language should be echoed
* instead of producing silence.
*/
echoTargetLanguage: z.boolean().optional(),
}),
),
);
export type GoogleSpeechTranslationModelOptions = InferSchema<
typeof googleSpeechTranslationModelOptions
>;
import {
InvalidArgumentError,
type Experimental_SpeechTranslationModelV4 as SpeechTranslationModelV4,
type Experimental_SpeechTranslationModelV4StreamOptions as SpeechTranslationModelV4StreamOptions,
type Experimental_SpeechTranslationModelV4StreamPart as SpeechTranslationModelV4StreamPart,
type Experimental_SpeechTranslationModelV4Usage as SpeechTranslationModelV4Usage,
type SharedV4Warning,
} from '@ai-sdk/provider';
import {
connectToWebSocket,
combineHeaders,
convertBase64ToUint8Array,
convertToBase64,
parseProviderOptions,
safeParseJSON,
serializeModelOptions,
WORKFLOW_DESERIALIZE,
WORKFLOW_SERIALIZE,
waitForWebSocketBufferDrain,
type WebSocketConnection,
type WebSocketConstructor,
type WebSocketLike,
} from '@ai-sdk/provider-utils';
import { getModelPath } from '../get-model-path';
import { getRealtimeWebSocketURL } from '../get-realtime-base-url';
import {
googleSpeechTranslationModelOptions,
type GoogleSpeechTranslationModelId,
type GoogleSpeechTranslationModelOptions,
} from './google-speech-translation-model-options';
const liveWebSocketPath =
'google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent';
/**
* After the input audio has ended, finish after this much trailing output
* silence. Live Translation is continuous and does not emit turnComplete.
*/
const defaultFinishGraceMs = 1000;
const googleLiveOutputAudioRate = 24000;
const pcm16SilenceAmplitudeThreshold = 128;
function getLiveWebSocketURL(baseURL: string, apiKey: string): URL {
const url = getRealtimeWebSocketURL(baseURL, liveWebSocketPath);
url.searchParams.set('key', apiKey);
return url;
}
type GoogleLiveTokensDetail = {
modality?: string;
tokenCount?: number;
};
type GoogleLiveServerMessage = {
setupComplete?: unknown;
serverContent?: {
modelTurn?: {
parts?: Array<{
inlineData?: { data?: string };
}>;
};
outputTranscription?: { text?: string };
inputTranscription?: { text?: string };
turnComplete?: boolean;
};
inputTranscription?: { text?: string };
usageMetadata?: {
promptTokensDetails?: GoogleLiveTokensDetail[];
responseTokensDetails?: GoogleLiveTokensDetail[];
};
error?: { message?: string };
};
export type GoogleSpeechTranslationModelConfig = {
provider: string;
baseURL: string;
headers: () => Record<string, string | undefined>;
webSocket?: WebSocketConstructor;
_internal?: {
currentDate?: () => Date;
finishGraceMs?: number;
};
};
export class GoogleSpeechTranslationModel implements SpeechTranslationModelV4 {
readonly specificationVersion = 'v4';
readonly modelId: GoogleSpeechTranslationModelId;
private readonly config: GoogleSpeechTranslationModelConfig;
static [WORKFLOW_SERIALIZE](model: GoogleSpeechTranslationModel) {
return serializeModelOptions({
modelId: model.modelId,
config: model.config,
});
}
static [WORKFLOW_DESERIALIZE](options: {
modelId: GoogleSpeechTranslationModelId;
config: GoogleSpeechTranslationModelConfig;
}) {
return new GoogleSpeechTranslationModel(options.modelId, options.config);
}
get provider(): string {
return this.config.provider;
}
constructor(
modelId: GoogleSpeechTranslationModelId,
config: GoogleSpeechTranslationModelConfig,
) {
this.modelId = modelId;
this.config = config;
}
async doStream(
options: SpeechTranslationModelV4StreamOptions,
): Promise<Awaited<ReturnType<SpeechTranslationModelV4['doStream']>>> {
if (options.targetLanguage == null) {
throw new InvalidArgumentError({
argument: 'targetLanguage',
message: `targetLanguage is required for translation model '${this.modelId}'.`,
});
}
const currentDate = this.config._internal?.currentDate?.() ?? new Date();
const googleOptions = await parseProviderOptions({
provider: 'google',
providerOptions: options.providerOptions,
schema: googleSpeechTranslationModelOptions,
});
const warnings: SharedV4Warning[] = [];
validateGoogleSpeechTranslationInputAudioFormat(options.inputAudioFormat);
if (options.sourceLanguage != null) {
warnings.push({
type: 'unsupported',
feature: 'sourceLanguage',
details:
'The Gemini Live translation API auto-detects the source language and does not accept a source language.',
});
}
if (options.outputAudioFormat != null) {
warnings.push({
type: 'unsupported',
feature: 'outputAudioFormat',
details:
'The Gemini Live API always outputs 24kHz 16-bit PCM audio and does not accept an output audio format.',
});
}
const headers = combineHeaders(this.config.headers(), options.headers);
// last case-variant wins: combineHeaders keeps case-distinct keys and
// spreads per-call headers after configuration headers
let apiKey: string | undefined;
for (const [key, value] of Object.entries(headers)) {
if (key.toLowerCase() === 'x-goog-api-key' && value != null) {
apiKey = value;
}
}
if (apiKey == null) {
throw new Error(
'Google Generative AI API key is required for streaming translation.',
);
}
const webSocketHeaders = Object.fromEntries(
Object.entries(headers).filter(
([key]) => key.toLowerCase() !== 'x-goog-api-key',
),
);
const setup = buildGoogleLiveSpeechTranslationSetup({
modelId: this.modelId,
targetLanguage: options.targetLanguage,
providerOptions: googleOptions,
});
return {
request: { body: setup },
response: {
timestamp: currentDate,
modelId: this.modelId,
},
stream: createGoogleLiveSpeechTranslationStream({
webSocket: this.config.webSocket,
url: getLiveWebSocketURL(this.config.baseURL, apiKey),
headers: webSocketHeaders,
setup,
inputAudioRate: options.inputAudioFormat.rate ?? 16000,
finishGraceMs:
this.config._internal?.finishGraceMs ?? defaultFinishGraceMs,
warnings,
audio: options.audio,
abortSignal: options.abortSignal,
includeRawChunks: options.includeRawChunks,
}),
};
}
}
function createGoogleLiveSpeechTranslationStream({
webSocket,
url,
headers,
setup,
inputAudioRate,
finishGraceMs,
warnings,
audio,
abortSignal,
includeRawChunks,
}: {
webSocket: WebSocketConstructor | undefined;
url: URL;
headers: Record<string, string | undefined>;
setup: unknown;
inputAudioRate: number;
finishGraceMs: number;
warnings: SharedV4Warning[];
audio: ReadableStream<Uint8Array | string>;
abortSignal: AbortSignal | undefined;
includeRawChunks: boolean | undefined;
}) {
let finished = false;
let cleanup: (closeCode?: number) => void = () => {};
return new ReadableStream<SpeechTranslationModelV4StreamPart>({
start: controller => {
let audioReader:
| ReadableStreamDefaultReader<Uint8Array | string>
| undefined;
let connection: WebSocketConnection | undefined;
// The Live API contract requires waiting for the `setupComplete`
// server message before sending realtime input: the audio send loop
// is gated on this promise.
let resolveSetupComplete!: () => void;
const setupComplete = new Promise<void>(resolve => {
resolveSetupComplete = resolve;
});
// Google Live messages carry no response/item IDs; a turn counter
// generates consistent synthetic IDs (like the realtime event mapper).
let turnCounter = 0;
// Transcription fragments arrive incrementally and are accumulated per
// turn; `turnComplete` finalizes the current turn.
let sourceText = '';
let sourceTurnBuffer = '';
let translationText = '';
let translationTurnBuffer = '';
let audioEnded = false;
let usage: SpeechTranslationModelV4Usage | undefined;
// Live Translation is a continuous pipeline rather than a turn-based
// model. After audioStreamEnd it keeps sending PCM silence indefinitely
// and does not emit turnComplete. Drain translated speech, then finish
// after enough trailing silence. Keep turnComplete handling as a
// fallback for compatible server implementations and test doubles.
let openTurn = false;
let sawTurnComplete = false;
let trailingSilenceMs = 0;
let finishTimer: ReturnType<typeof setTimeout> | undefined;
const itemId = () => `google-item-${turnCounter}`;
const cancelPendingFinish = () => {
if (finishTimer != null) {
clearTimeout(finishTimer);
finishTimer = undefined;
}
};
const schedulePendingFinish = () => {
if (finished || finishTimer != null) return;
finishTimer = setTimeout(() => {
finishTimer = undefined;
finish();
}, finishGraceMs);
};
const onTurnActivity = () => {
openTurn = true;
trailingSilenceMs = 0;
cancelPendingFinish();
};
cleanup = (closeCode?: number) => {
cancelPendingFinish();
if (audioReader != null) {
void audioReader.cancel().catch(() => {});
} else {
// pre-open failure or abort: cancel the caller's audio stream so an
// upstream producer piping into it does not hang:
void audio.cancel().catch(() => {});
}
connection?.close(closeCode);
};
const finishWithError = (error: unknown) => {
if (finished) return;
finished = true;
cleanup();
controller.error(error);
};
const finish = () => {
if (finished) return;
if (sourceTurnBuffer !== '' || translationTurnBuffer !== '') {
completeTurn();
}
finished = true;
controller.enqueue({
type: 'finish',
sourceText,
outputText: translationText,
usage,
});
controller.close();
cleanup(1000);
};
const completeTurn = () => {
if (sourceTurnBuffer !== '') {
controller.enqueue({
type: 'source-transcript-final',
id: itemId(),
text: sourceTurnBuffer,
});
sourceText += sourceTurnBuffer;
sourceTurnBuffer = '';
}
if (translationTurnBuffer !== '') {
controller.enqueue({
type: 'output-text-final',
id: itemId(),
text: translationTurnBuffer,
});
translationText += translationTurnBuffer;
translationTurnBuffer = '';
}
turnCounter++;
};
const sendAudio = async (socket: WebSocketLike) => {
audioReader = audio.getReader();
try {
while (true) {
const { done, value } = await audioReader.read();
if (done || finished) break;
socket.send(
JSON.stringify({
realtimeInput: {
audio: {
data: convertToBase64(value),
mimeType: `audio/pcm;rate=${inputAudioRate}`,
},
},
}),
);
// backpressure: pause reads while the socket buffer is full
await waitForWebSocketBufferDrain(socket);
}
} finally {
audioReader.releaseLock();
// unlocked again: cleanup must cancel `audio`, not the reader
audioReader = undefined;
}
if (!finished) {
socket.send(
JSON.stringify({ realtimeInput: { audioStreamEnd: true } }),
);
audioEnded = true;
// a turnComplete already received after the final audio chunk
// satisfies the finish condition:
if (sawTurnComplete && !openTurn) {
schedulePendingFinish();
}
}
};
connection = connectToWebSocket({
url,
headers,
webSocket,
abortSignal,
onAbort: finishWithError,
onProcessingError: finishWithError,
onOpen: socket => {
controller.enqueue({ type: 'stream-start', warnings });
socket.send(JSON.stringify({ setup }));
// audio may only be sent after the server acknowledged the setup:
void setupComplete
.then(() => (finished ? undefined : sendAudio(socket)))
.catch(finishWithError);
},
onMessageText: async text => {
if (finished) return;
const parsed = await safeParseJSON({ text });
if (!parsed.success) return;
const message = parsed.value as GoogleLiveServerMessage;
if (includeRawChunks) {
controller.enqueue({ type: 'raw', rawValue: message });
}
if (message.setupComplete != null) {
resolveSetupComplete();
}
if (message.usageMetadata != null) {
usage = accumulateGoogleLiveUsage(usage, message.usageMetadata);
}
if (message.error != null) {
finishWithError(
new Error(message.error.message ?? 'Google Live API error'),
);
return;
}
const inputTranscriptionText =
message.serverContent?.inputTranscription?.text ??
message.inputTranscription?.text;
if (inputTranscriptionText) {
onTurnActivity();
sourceTurnBuffer += inputTranscriptionText;
controller.enqueue({
type: 'source-transcript-delta',
id: itemId(),
delta: inputTranscriptionText,
});
}
const serverContent = message.serverContent;
if (serverContent == null) {
return;
}
for (const part of serverContent.modelTurn?.parts ?? []) {
if (part.inlineData?.data) {
controller.enqueue({
type: 'audio',
id: itemId(),
audio: part.inlineData.data,
});
const silenceDurationMs = getPcm16SilenceDurationMs(
part.inlineData.data,
);
if (audioEnded && silenceDurationMs != null) {
trailingSilenceMs += silenceDurationMs;
if (trailingSilenceMs >= finishGraceMs) {
finish();
return;
}
} else {
onTurnActivity();
}
}
}
if (serverContent.outputTranscription?.text) {
onTurnActivity();
translationTurnBuffer += serverContent.outputTranscription.text;
controller.enqueue({
type: 'output-text-delta',
id: itemId(),
delta: serverContent.outputTranscription.text,
});
}
if (serverContent.turnComplete) {
completeTurn();
openTurn = false;
sawTurnComplete = true;
if (audioEnded) {
schedulePendingFinish();
}
}
},
onSocketError: () => {
finishWithError(new Error('Google Live translation error'));
},
onClose: ({ code, reason }) => {
if (finished) return;
// a close while a finish is pending confirms that no further turn
// activity follows:
if (finishTimer != null) {
finish();
return;
}
// a close before the finish condition was reached is an abnormal
// termination: surface the close diagnostics
finishWithError(
new Error(
`Google Live translation WebSocket closed unexpectedly before finishing` +
` (code ${code ?? 'unknown'}${reason ? `, reason: ${reason}` : ''}).`,
),
);
},
});
},
cancel: () => {
if (finished) return;
finished = true;
cleanup();
},
});
}
function accumulateGoogleLiveUsage(
usage: SpeechTranslationModelV4Usage | undefined,
usageMetadata: {
promptTokensDetails?: GoogleLiveTokensDetail[];
responseTokensDetails?: GoogleLiveTokensDetail[];
},
): SpeechTranslationModelV4Usage | undefined {
let inputAudioTokens = usage?.inputAudioTokens;
let outputAudioTokens = usage?.outputAudioTokens;
// Live Translation emits periodic usage deltas. Its TEXT prompt detail is
// internal translation context (the public input is audio-only), so only
// aggregate the billable input/output audio modalities.
for (const detail of usageMetadata.promptTokensDetails ?? []) {
if (detail.modality === 'AUDIO' && detail.tokenCount != null) {
inputAudioTokens = (inputAudioTokens ?? 0) + detail.tokenCount;
}
}
for (const detail of usageMetadata.responseTokensDetails ?? []) {
if (detail.modality === 'AUDIO' && detail.tokenCount != null) {
outputAudioTokens = (outputAudioTokens ?? 0) + detail.tokenCount;
}
}
if (inputAudioTokens == null && outputAudioTokens == null) {
return usage;
}
return {
...usage,
...(inputAudioTokens != null ? { inputAudioTokens } : {}),
...(outputAudioTokens != null ? { outputAudioTokens } : {}),
};
}
function getPcm16SilenceDurationMs(audio: string): number | undefined {
let bytes: Uint8Array;
try {
bytes = convertBase64ToUint8Array(audio);
} catch {
return undefined;
}
if (bytes.byteLength < 2) {
return undefined;
}
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const sampleCount = Math.floor(bytes.byteLength / 2);
for (let i = 0; i < sampleCount; i++) {
if (Math.abs(view.getInt16(i * 2, true)) > pcm16SilenceAmplitudeThreshold) {
return undefined;
}
}
return (sampleCount / googleLiveOutputAudioRate) * 1000;
}
function buildGoogleLiveSpeechTranslationSetup({
modelId,
targetLanguage,
providerOptions,
}: {
modelId: string;
targetLanguage: string;
providerOptions: GoogleSpeechTranslationModelOptions | undefined;
}) {
return {
model: getModelPath(modelId),
generationConfig: {
responseModalities: ['AUDIO'],
translationConfig: {
targetLanguageCode: targetLanguage,
...(providerOptions?.echoTargetLanguage != null
? { echoTargetLanguage: providerOptions.echoTargetLanguage }
: {}),
},
},
inputAudioTranscription: {},
outputAudioTranscription: {},
};
}
function validateGoogleSpeechTranslationInputAudioFormat(
inputAudioFormat: SpeechTranslationModelV4StreamOptions['inputAudioFormat'],
) {
if (
inputAudioFormat.type !== 'audio/pcm' ||
(inputAudioFormat.rate != null && inputAudioFormat.rate !== 16000)
) {
throw new InvalidArgumentError({
argument: 'inputAudioFormat',
message:
'The Gemini Live translation API only supports 16kHz 16-bit PCM input audio.',
});
}
}
+14
-14

@@ -514,7 +514,7 @@ import * as _ai_sdk_provider_utils from '@ai-sdk/provider-utils';

type GoogleTranslationModelId = 'gemini-3.5-live-translate-preview' | (string & {});
declare const googleTranslationModelOptions: _ai_sdk_provider_utils.LazySchema<{
type GoogleSpeechTranslationModelId = 'gemini-3.5-live-translate-preview' | (string & {});
declare const googleSpeechTranslationModelOptions: _ai_sdk_provider_utils.LazySchema<{
echoTargetLanguage?: boolean | undefined;
}>;
type GoogleTranslationModelOptions = InferSchema<typeof googleTranslationModelOptions>;
type GoogleSpeechTranslationModelOptions = InferSchema<typeof googleSpeechTranslationModelOptions>;

@@ -560,7 +560,7 @@ interface GoogleProvider extends ProviderV4 {

*/
translation(modelId: GoogleTranslationModelId): Experimental_SpeechTranslationModelV4;
translation(modelId: GoogleSpeechTranslationModelId): Experimental_SpeechTranslationModelV4;
/**
* Creates an experimental model for streaming speech translation.
*/
speechTranslationModel(modelId: GoogleTranslationModelId): Experimental_SpeechTranslationModelV4;
speechTranslationModel(modelId: GoogleSpeechTranslationModelId): Experimental_SpeechTranslationModelV4;
/**

@@ -683,3 +683,3 @@ * Creates a model for speech generation (text-to-speech).

type GoogleTranslationModelConfig = {
type GoogleSpeechTranslationModelConfig = {
provider: string;

@@ -694,7 +694,7 @@ baseURL: string;

};
declare class GoogleTranslationModel implements Experimental_SpeechTranslationModelV4 {
declare class GoogleSpeechTranslationModel implements Experimental_SpeechTranslationModelV4 {
readonly specificationVersion = "v4";
readonly modelId: GoogleTranslationModelId;
readonly modelId: GoogleSpeechTranslationModelId;
private readonly config;
static [WORKFLOW_SERIALIZE](model: GoogleTranslationModel): {
static [WORKFLOW_SERIALIZE](model: GoogleSpeechTranslationModel): {
modelId: string;

@@ -704,7 +704,7 @@ config: _ai_sdk_provider.JSONObject;

static [WORKFLOW_DESERIALIZE](options: {
modelId: GoogleTranslationModelId;
config: GoogleTranslationModelConfig;
}): GoogleTranslationModel;
modelId: GoogleSpeechTranslationModelId;
config: GoogleSpeechTranslationModelConfig;
}): GoogleSpeechTranslationModel;
get provider(): string;
constructor(modelId: GoogleTranslationModelId, config: GoogleTranslationModelConfig);
constructor(modelId: GoogleSpeechTranslationModelId, config: GoogleSpeechTranslationModelConfig);
doStream(options: Experimental_SpeechTranslationModelV4StreamOptions): Promise<Awaited<ReturnType<Experimental_SpeechTranslationModelV4['doStream']>>>;

@@ -715,2 +715,2 @@ }

export { GoogleRealtimeModel as Experimental_GoogleRealtimeModel, type GoogleRealtimeModelConfig as Experimental_GoogleRealtimeModelConfig, type GoogleRealtimeModelId as Experimental_GoogleRealtimeModelId, type GoogleRealtimeModelOptions as Experimental_GoogleRealtimeModelOptions, GoogleTranslationModel as Experimental_GoogleTranslationModel, type GoogleTranslationModelConfig as Experimental_GoogleTranslationModelConfig, type GoogleTranslationModelId as Experimental_GoogleTranslationModelId, type GoogleTranslationModelOptions as Experimental_GoogleTranslationModelOptions, type GoogleEmbeddingModelOptions, type GoogleErrorData, type GoogleFilesUploadOptions, type GoogleEmbeddingModelOptions as GoogleGenerativeAIEmbeddingProviderOptions, type GoogleImageModelOptions as GoogleGenerativeAIImageProviderOptions, type GoogleProvider as GoogleGenerativeAIProvider, type GoogleProviderMetadata as GoogleGenerativeAIProviderMetadata, type GoogleLanguageModelOptions as GoogleGenerativeAIProviderOptions, type GoogleProviderSettings as GoogleGenerativeAIProviderSettings, type GoogleVideoModelId as GoogleGenerativeAIVideoModelId, type GoogleVideoModelOptions as GoogleGenerativeAIVideoProviderOptions, type GoogleImageModelOptions, type GoogleInteractionsAgentName, type GoogleInteractionsModelId, type GoogleInteractionsProviderMetadata, type GoogleLanguageModelInteractionsOptions, type GoogleLanguageModelOptions, type GoogleProvider, type GoogleProviderMetadata, type GoogleProviderSettings, type GoogleSpeechModelId, type GoogleSpeechModelOptions, type GoogleVideoModelId, type GoogleVideoModelOptions, VERSION, createGoogle, createGoogle as createGoogleGenerativeAI, google };
export { GoogleRealtimeModel as Experimental_GoogleRealtimeModel, type GoogleRealtimeModelConfig as Experimental_GoogleRealtimeModelConfig, type GoogleRealtimeModelId as Experimental_GoogleRealtimeModelId, type GoogleRealtimeModelOptions as Experimental_GoogleRealtimeModelOptions, GoogleSpeechTranslationModel as Experimental_GoogleSpeechTranslationModel, type GoogleSpeechTranslationModelConfig as Experimental_GoogleSpeechTranslationModelConfig, type GoogleSpeechTranslationModelId as Experimental_GoogleSpeechTranslationModelId, type GoogleSpeechTranslationModelOptions as Experimental_GoogleSpeechTranslationModelOptions, GoogleSpeechTranslationModel as Experimental_GoogleTranslationModel, type GoogleSpeechTranslationModelConfig as Experimental_GoogleTranslationModelConfig, type GoogleSpeechTranslationModelId as Experimental_GoogleTranslationModelId, type GoogleSpeechTranslationModelOptions as Experimental_GoogleTranslationModelOptions, type GoogleEmbeddingModelOptions, type GoogleErrorData, type GoogleFilesUploadOptions, type GoogleEmbeddingModelOptions as GoogleGenerativeAIEmbeddingProviderOptions, type GoogleImageModelOptions as GoogleGenerativeAIImageProviderOptions, type GoogleProvider as GoogleGenerativeAIProvider, type GoogleProviderMetadata as GoogleGenerativeAIProviderMetadata, type GoogleLanguageModelOptions as GoogleGenerativeAIProviderOptions, type GoogleProviderSettings as GoogleGenerativeAIProviderSettings, type GoogleVideoModelId as GoogleGenerativeAIVideoModelId, type GoogleVideoModelOptions as GoogleGenerativeAIVideoProviderOptions, type GoogleImageModelOptions, type GoogleInteractionsAgentName, type GoogleInteractionsModelId, type GoogleInteractionsProviderMetadata, type GoogleLanguageModelInteractionsOptions, type GoogleLanguageModelOptions, type GoogleProvider, type GoogleProviderMetadata, type GoogleProviderSettings, type GoogleSpeechModelId, type GoogleSpeechModelOptions, type GoogleVideoModelId, type GoogleVideoModelOptions, VERSION, createGoogle, createGoogle as createGoogleGenerativeAI, google };
{
"name": "@ai-sdk/google",
"version": "4.0.36",
"version": "4.0.37",
"type": "module",

@@ -5,0 +5,0 @@ "license": "Apache-2.0",

@@ -45,4 +45,4 @@ import type {

import { GoogleRealtimeModel } from './realtime/google-realtime-model';
import { GoogleTranslationModel } from './translation/google-translation-model';
import type { GoogleTranslationModelId } from './translation/google-translation-model-options';
import { GoogleSpeechTranslationModel } from './speech-translation/google-speech-translation-model';
import type { GoogleSpeechTranslationModelId } from './speech-translation/google-speech-translation-model-options';

@@ -102,3 +102,5 @@ export interface GoogleProvider extends ProviderV4 {

*/
translation(modelId: GoogleTranslationModelId): SpeechTranslationModelV4;
translation(
modelId: GoogleSpeechTranslationModelId,
): SpeechTranslationModelV4;

@@ -109,3 +111,3 @@ /**

speechTranslationModel(
modelId: GoogleTranslationModelId,
modelId: GoogleSpeechTranslationModelId,
): SpeechTranslationModelV4;

@@ -316,5 +318,7 @@

const createTranslationModel = (modelId: GoogleTranslationModelId) =>
new GoogleTranslationModel(modelId, {
provider: `${providerName}.translation`,
const createSpeechTranslationModel = (
modelId: GoogleSpeechTranslationModelId,
) =>
new GoogleSpeechTranslationModel(modelId, {
provider: `${providerName}.speech-translation`,
baseURL,

@@ -395,4 +399,4 @@ headers: getHeaders,

provider.speechModel = createSpeechModel;
provider.translation = createTranslationModel;
provider.speechTranslationModel = createTranslationModel;
provider.translation = createSpeechTranslationModel;
provider.speechTranslationModel = createSpeechTranslationModel;
provider.interactions = createInteractionsModel;

@@ -399,0 +403,0 @@ provider.tools = googleTools;

@@ -63,9 +63,21 @@ export type { GoogleErrorData } from './google-error';

} from './realtime/google-realtime-model-options';
export { GoogleTranslationModel as Experimental_GoogleTranslationModel } from './translation/google-translation-model';
export type { GoogleTranslationModelConfig as Experimental_GoogleTranslationModelConfig } from './translation/google-translation-model';
export {
GoogleSpeechTranslationModel as Experimental_GoogleSpeechTranslationModel,
/** @deprecated Use `Experimental_GoogleSpeechTranslationModel` instead. */
GoogleSpeechTranslationModel as Experimental_GoogleTranslationModel,
} from './speech-translation/google-speech-translation-model';
export type {
GoogleTranslationModelId as Experimental_GoogleTranslationModelId,
GoogleTranslationModelOptions as Experimental_GoogleTranslationModelOptions,
} from './translation/google-translation-model-options';
GoogleSpeechTranslationModelConfig as Experimental_GoogleSpeechTranslationModelConfig,
/** @deprecated Use `Experimental_GoogleSpeechTranslationModelConfig` instead. */
GoogleSpeechTranslationModelConfig as Experimental_GoogleTranslationModelConfig,
} from './speech-translation/google-speech-translation-model';
export type {
GoogleSpeechTranslationModelId as Experimental_GoogleSpeechTranslationModelId,
/** @deprecated Use `Experimental_GoogleSpeechTranslationModelId` instead. */
GoogleSpeechTranslationModelId as Experimental_GoogleTranslationModelId,
GoogleSpeechTranslationModelOptions as Experimental_GoogleSpeechTranslationModelOptions,
/** @deprecated Use `Experimental_GoogleSpeechTranslationModelOptions` instead. */
GoogleSpeechTranslationModelOptions as Experimental_GoogleTranslationModelOptions,
} from './speech-translation/google-speech-translation-model-options';
export { VERSION } from './version';
import {
lazySchema,
zodSchema,
type InferSchema,
} from '@ai-sdk/provider-utils';
import { z } from 'zod/v4';
export type GoogleTranslationModelId =
| 'gemini-3.5-live-translate-preview'
| (string & {});
export const googleTranslationModelOptions = lazySchema(() =>
zodSchema(
z.object({
/**
* Whether input audio already in the target language should be echoed
* instead of producing silence.
*/
echoTargetLanguage: z.boolean().optional(),
}),
),
);
export type GoogleTranslationModelOptions = InferSchema<
typeof googleTranslationModelOptions
>;
import {
InvalidArgumentError,
type Experimental_SpeechTranslationModelV4 as TranslationModelV4,
type Experimental_SpeechTranslationModelV4StreamOptions as SpeechTranslationModelV4StreamOptions,
type Experimental_SpeechTranslationModelV4StreamPart as SpeechTranslationModelV4StreamPart,
type Experimental_SpeechTranslationModelV4Usage as SpeechTranslationModelV4Usage,
type SharedV4Warning,
} from '@ai-sdk/provider';
import {
connectToWebSocket,
combineHeaders,
convertBase64ToUint8Array,
convertToBase64,
parseProviderOptions,
safeParseJSON,
serializeModelOptions,
WORKFLOW_DESERIALIZE,
WORKFLOW_SERIALIZE,
waitForWebSocketBufferDrain,
type WebSocketConnection,
type WebSocketConstructor,
type WebSocketLike,
} from '@ai-sdk/provider-utils';
import { getModelPath } from '../get-model-path';
import { getRealtimeWebSocketURL } from '../get-realtime-base-url';
import {
googleTranslationModelOptions,
type GoogleTranslationModelId,
type GoogleTranslationModelOptions,
} from './google-translation-model-options';
const liveWebSocketPath =
'google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent';
/**
* After the input audio has ended, finish after this much trailing output
* silence. Live Translation is continuous and does not emit turnComplete.
*/
const defaultFinishGraceMs = 1000;
const googleLiveOutputAudioRate = 24000;
const pcm16SilenceAmplitudeThreshold = 128;
function getLiveWebSocketURL(baseURL: string, apiKey: string): URL {
const url = getRealtimeWebSocketURL(baseURL, liveWebSocketPath);
url.searchParams.set('key', apiKey);
return url;
}
type GoogleLiveTokensDetail = {
modality?: string;
tokenCount?: number;
};
type GoogleLiveServerMessage = {
setupComplete?: unknown;
serverContent?: {
modelTurn?: {
parts?: Array<{
inlineData?: { data?: string };
}>;
};
outputTranscription?: { text?: string };
inputTranscription?: { text?: string };
turnComplete?: boolean;
};
inputTranscription?: { text?: string };
usageMetadata?: {
promptTokensDetails?: GoogleLiveTokensDetail[];
responseTokensDetails?: GoogleLiveTokensDetail[];
};
error?: { message?: string };
};
export type GoogleTranslationModelConfig = {
provider: string;
baseURL: string;
headers: () => Record<string, string | undefined>;
webSocket?: WebSocketConstructor;
_internal?: {
currentDate?: () => Date;
finishGraceMs?: number;
};
};
export class GoogleTranslationModel implements TranslationModelV4 {
readonly specificationVersion = 'v4';
readonly modelId: GoogleTranslationModelId;
private readonly config: GoogleTranslationModelConfig;
static [WORKFLOW_SERIALIZE](model: GoogleTranslationModel) {
return serializeModelOptions({
modelId: model.modelId,
config: model.config,
});
}
static [WORKFLOW_DESERIALIZE](options: {
modelId: GoogleTranslationModelId;
config: GoogleTranslationModelConfig;
}) {
return new GoogleTranslationModel(options.modelId, options.config);
}
get provider(): string {
return this.config.provider;
}
constructor(
modelId: GoogleTranslationModelId,
config: GoogleTranslationModelConfig,
) {
this.modelId = modelId;
this.config = config;
}
async doStream(
options: SpeechTranslationModelV4StreamOptions,
): Promise<Awaited<ReturnType<TranslationModelV4['doStream']>>> {
if (options.targetLanguage == null) {
throw new InvalidArgumentError({
argument: 'targetLanguage',
message: `targetLanguage is required for translation model '${this.modelId}'.`,
});
}
const currentDate = this.config._internal?.currentDate?.() ?? new Date();
const googleOptions = await parseProviderOptions({
provider: 'google',
providerOptions: options.providerOptions,
schema: googleTranslationModelOptions,
});
const warnings: SharedV4Warning[] = [];
validateGoogleTranslationInputAudioFormat(options.inputAudioFormat);
if (options.sourceLanguage != null) {
warnings.push({
type: 'unsupported',
feature: 'sourceLanguage',
details:
'The Gemini Live translation API auto-detects the source language and does not accept a source language.',
});
}
if (options.outputAudioFormat != null) {
warnings.push({
type: 'unsupported',
feature: 'outputAudioFormat',
details:
'The Gemini Live API always outputs 24kHz 16-bit PCM audio and does not accept an output audio format.',
});
}
const headers = combineHeaders(this.config.headers(), options.headers);
// last case-variant wins: combineHeaders keeps case-distinct keys and
// spreads per-call headers after configuration headers
let apiKey: string | undefined;
for (const [key, value] of Object.entries(headers)) {
if (key.toLowerCase() === 'x-goog-api-key' && value != null) {
apiKey = value;
}
}
if (apiKey == null) {
throw new Error(
'Google Generative AI API key is required for streaming translation.',
);
}
const webSocketHeaders = Object.fromEntries(
Object.entries(headers).filter(
([key]) => key.toLowerCase() !== 'x-goog-api-key',
),
);
const setup = buildGoogleLiveTranslationSetup({
modelId: this.modelId,
targetLanguage: options.targetLanguage,
providerOptions: googleOptions,
});
return {
request: { body: setup },
response: {
timestamp: currentDate,
modelId: this.modelId,
},
stream: createGoogleLiveTranslationStream({
webSocket: this.config.webSocket,
url: getLiveWebSocketURL(this.config.baseURL, apiKey),
headers: webSocketHeaders,
setup,
inputAudioRate: options.inputAudioFormat.rate ?? 16000,
finishGraceMs:
this.config._internal?.finishGraceMs ?? defaultFinishGraceMs,
warnings,
audio: options.audio,
abortSignal: options.abortSignal,
includeRawChunks: options.includeRawChunks,
}),
};
}
}
function createGoogleLiveTranslationStream({
webSocket,
url,
headers,
setup,
inputAudioRate,
finishGraceMs,
warnings,
audio,
abortSignal,
includeRawChunks,
}: {
webSocket: WebSocketConstructor | undefined;
url: URL;
headers: Record<string, string | undefined>;
setup: unknown;
inputAudioRate: number;
finishGraceMs: number;
warnings: SharedV4Warning[];
audio: ReadableStream<Uint8Array | string>;
abortSignal: AbortSignal | undefined;
includeRawChunks: boolean | undefined;
}) {
let finished = false;
let cleanup: (closeCode?: number) => void = () => {};
return new ReadableStream<SpeechTranslationModelV4StreamPart>({
start: controller => {
let audioReader:
| ReadableStreamDefaultReader<Uint8Array | string>
| undefined;
let connection: WebSocketConnection | undefined;
// The Live API contract requires waiting for the `setupComplete`
// server message before sending realtime input: the audio send loop
// is gated on this promise.
let resolveSetupComplete!: () => void;
const setupComplete = new Promise<void>(resolve => {
resolveSetupComplete = resolve;
});
// Google Live messages carry no response/item IDs; a turn counter
// generates consistent synthetic IDs (like the realtime event mapper).
let turnCounter = 0;
// Transcription fragments arrive incrementally and are accumulated per
// turn; `turnComplete` finalizes the current turn.
let sourceText = '';
let sourceTurnBuffer = '';
let translationText = '';
let translationTurnBuffer = '';
let audioEnded = false;
let usage: SpeechTranslationModelV4Usage | undefined;
// Live Translation is a continuous pipeline rather than a turn-based
// model. After audioStreamEnd it keeps sending PCM silence indefinitely
// and does not emit turnComplete. Drain translated speech, then finish
// after enough trailing silence. Keep turnComplete handling as a
// fallback for compatible server implementations and test doubles.
let openTurn = false;
let sawTurnComplete = false;
let trailingSilenceMs = 0;
let finishTimer: ReturnType<typeof setTimeout> | undefined;
const itemId = () => `google-item-${turnCounter}`;
const cancelPendingFinish = () => {
if (finishTimer != null) {
clearTimeout(finishTimer);
finishTimer = undefined;
}
};
const schedulePendingFinish = () => {
if (finished || finishTimer != null) return;
finishTimer = setTimeout(() => {
finishTimer = undefined;
finish();
}, finishGraceMs);
};
const onTurnActivity = () => {
openTurn = true;
trailingSilenceMs = 0;
cancelPendingFinish();
};
cleanup = (closeCode?: number) => {
cancelPendingFinish();
if (audioReader != null) {
void audioReader.cancel().catch(() => {});
} else {
// pre-open failure or abort: cancel the caller's audio stream so an
// upstream producer piping into it does not hang:
void audio.cancel().catch(() => {});
}
connection?.close(closeCode);
};
const finishWithError = (error: unknown) => {
if (finished) return;
finished = true;
cleanup();
controller.error(error);
};
const finish = () => {
if (finished) return;
if (sourceTurnBuffer !== '' || translationTurnBuffer !== '') {
completeTurn();
}
finished = true;
controller.enqueue({
type: 'finish',
sourceText,
outputText: translationText,
usage,
});
controller.close();
cleanup(1000);
};
const completeTurn = () => {
if (sourceTurnBuffer !== '') {
controller.enqueue({
type: 'source-transcript-final',
id: itemId(),
text: sourceTurnBuffer,
});
sourceText += sourceTurnBuffer;
sourceTurnBuffer = '';
}
if (translationTurnBuffer !== '') {
controller.enqueue({
type: 'output-text-final',
id: itemId(),
text: translationTurnBuffer,
});
translationText += translationTurnBuffer;
translationTurnBuffer = '';
}
turnCounter++;
};
const sendAudio = async (socket: WebSocketLike) => {
audioReader = audio.getReader();
try {
while (true) {
const { done, value } = await audioReader.read();
if (done || finished) break;
socket.send(
JSON.stringify({
realtimeInput: {
audio: {
data: convertToBase64(value),
mimeType: `audio/pcm;rate=${inputAudioRate}`,
},
},
}),
);
// backpressure: pause reads while the socket buffer is full
await waitForWebSocketBufferDrain(socket);
}
} finally {
audioReader.releaseLock();
// unlocked again: cleanup must cancel `audio`, not the reader
audioReader = undefined;
}
if (!finished) {
socket.send(
JSON.stringify({ realtimeInput: { audioStreamEnd: true } }),
);
audioEnded = true;
// a turnComplete already received after the final audio chunk
// satisfies the finish condition:
if (sawTurnComplete && !openTurn) {
schedulePendingFinish();
}
}
};
connection = connectToWebSocket({
url,
headers,
webSocket,
abortSignal,
onAbort: finishWithError,
onProcessingError: finishWithError,
onOpen: socket => {
controller.enqueue({ type: 'stream-start', warnings });
socket.send(JSON.stringify({ setup }));
// audio may only be sent after the server acknowledged the setup:
void setupComplete
.then(() => (finished ? undefined : sendAudio(socket)))
.catch(finishWithError);
},
onMessageText: async text => {
if (finished) return;
const parsed = await safeParseJSON({ text });
if (!parsed.success) return;
const message = parsed.value as GoogleLiveServerMessage;
if (includeRawChunks) {
controller.enqueue({ type: 'raw', rawValue: message });
}
if (message.setupComplete != null) {
resolveSetupComplete();
}
if (message.usageMetadata != null) {
usage = accumulateGoogleLiveUsage(usage, message.usageMetadata);
}
if (message.error != null) {
finishWithError(
new Error(message.error.message ?? 'Google Live API error'),
);
return;
}
const inputTranscriptionText =
message.serverContent?.inputTranscription?.text ??
message.inputTranscription?.text;
if (inputTranscriptionText) {
onTurnActivity();
sourceTurnBuffer += inputTranscriptionText;
controller.enqueue({
type: 'source-transcript-delta',
id: itemId(),
delta: inputTranscriptionText,
});
}
const serverContent = message.serverContent;
if (serverContent == null) {
return;
}
for (const part of serverContent.modelTurn?.parts ?? []) {
if (part.inlineData?.data) {
controller.enqueue({
type: 'audio',
id: itemId(),
audio: part.inlineData.data,
});
const silenceDurationMs = getPcm16SilenceDurationMs(
part.inlineData.data,
);
if (audioEnded && silenceDurationMs != null) {
trailingSilenceMs += silenceDurationMs;
if (trailingSilenceMs >= finishGraceMs) {
finish();
return;
}
} else {
onTurnActivity();
}
}
}
if (serverContent.outputTranscription?.text) {
onTurnActivity();
translationTurnBuffer += serverContent.outputTranscription.text;
controller.enqueue({
type: 'output-text-delta',
id: itemId(),
delta: serverContent.outputTranscription.text,
});
}
if (serverContent.turnComplete) {
completeTurn();
openTurn = false;
sawTurnComplete = true;
if (audioEnded) {
schedulePendingFinish();
}
}
},
onSocketError: () => {
finishWithError(new Error('Google Live translation error'));
},
onClose: ({ code, reason }) => {
if (finished) return;
// a close while a finish is pending confirms that no further turn
// activity follows:
if (finishTimer != null) {
finish();
return;
}
// a close before the finish condition was reached is an abnormal
// termination: surface the close diagnostics
finishWithError(
new Error(
`Google Live translation WebSocket closed unexpectedly before finishing` +
` (code ${code ?? 'unknown'}${reason ? `, reason: ${reason}` : ''}).`,
),
);
},
});
},
cancel: () => {
if (finished) return;
finished = true;
cleanup();
},
});
}
function accumulateGoogleLiveUsage(
usage: SpeechTranslationModelV4Usage | undefined,
usageMetadata: {
promptTokensDetails?: GoogleLiveTokensDetail[];
responseTokensDetails?: GoogleLiveTokensDetail[];
},
): SpeechTranslationModelV4Usage | undefined {
let inputAudioTokens = usage?.inputAudioTokens;
let outputAudioTokens = usage?.outputAudioTokens;
// Live Translation emits periodic usage deltas. Its TEXT prompt detail is
// internal translation context (the public input is audio-only), so only
// aggregate the billable input/output audio modalities.
for (const detail of usageMetadata.promptTokensDetails ?? []) {
if (detail.modality === 'AUDIO' && detail.tokenCount != null) {
inputAudioTokens = (inputAudioTokens ?? 0) + detail.tokenCount;
}
}
for (const detail of usageMetadata.responseTokensDetails ?? []) {
if (detail.modality === 'AUDIO' && detail.tokenCount != null) {
outputAudioTokens = (outputAudioTokens ?? 0) + detail.tokenCount;
}
}
if (inputAudioTokens == null && outputAudioTokens == null) {
return usage;
}
return {
...usage,
...(inputAudioTokens != null ? { inputAudioTokens } : {}),
...(outputAudioTokens != null ? { outputAudioTokens } : {}),
};
}
function getPcm16SilenceDurationMs(audio: string): number | undefined {
let bytes: Uint8Array;
try {
bytes = convertBase64ToUint8Array(audio);
} catch {
return undefined;
}
if (bytes.byteLength < 2) {
return undefined;
}
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const sampleCount = Math.floor(bytes.byteLength / 2);
for (let i = 0; i < sampleCount; i++) {
if (Math.abs(view.getInt16(i * 2, true)) > pcm16SilenceAmplitudeThreshold) {
return undefined;
}
}
return (sampleCount / googleLiveOutputAudioRate) * 1000;
}
function buildGoogleLiveTranslationSetup({
modelId,
targetLanguage,
providerOptions,
}: {
modelId: string;
targetLanguage: string;
providerOptions: GoogleTranslationModelOptions | undefined;
}) {
return {
model: getModelPath(modelId),
generationConfig: {
responseModalities: ['AUDIO'],
translationConfig: {
targetLanguageCode: targetLanguage,
...(providerOptions?.echoTargetLanguage != null
? { echoTargetLanguage: providerOptions.echoTargetLanguage }
: {}),
},
},
inputAudioTranscription: {},
outputAudioTranscription: {},
};
}
function validateGoogleTranslationInputAudioFormat(
inputAudioFormat: SpeechTranslationModelV4StreamOptions['inputAudioFormat'],
) {
if (
inputAudioFormat.type !== 'audio/pcm' ||
(inputAudioFormat.rate != null && inputAudioFormat.rate !== 16000)
) {
throw new InvalidArgumentError({
argument: 'inputAudioFormat',
message:
'The Gemini Live translation API only supports 16kHz 16-bit PCM input audio.',
});
}
}

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display